From dff1304dbba3abeff6609ccdffcb9ede828f80c5 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Mon, 26 May 2025 20:37:24 +0530 Subject: [PATCH 001/119] Updated linting rules --- .eslintrc.js | 21 ++++--- src/cli.ts | 25 ++++---- src/collections/Extractors.ts | 42 ++++++------- src/collections/Groups.ts | 90 +++++++++++++-------------- src/collections/Requests.ts | 8 +-- src/collections/Tweet.ts | 12 ++-- src/enums/Api.ts | 2 +- src/enums/Authentication.ts | 2 +- src/enums/Data.ts | 2 +- src/enums/Logging.ts | 2 +- src/enums/Media.ts | 2 +- src/enums/Notification.ts | 2 +- src/enums/Resource.ts | 2 +- src/enums/Tweet.ts | 2 +- src/enums/raw/Analytics.ts | 4 +- src/enums/raw/Media.ts | 2 +- src/enums/raw/Notification.ts | 2 +- src/enums/raw/Tweet.ts | 4 +- src/models/RettiwtConfig.ts | 8 +-- src/models/args/FetchArgs.ts | 10 +-- src/models/auth/AuthCredential.ts | 10 +-- src/models/data/CursoredData.ts | 10 +-- src/models/data/Notification.ts | 12 ++-- src/models/data/Tweet.ts | 44 ++++++------- src/models/data/User.ts | 20 +++--- src/requests/Tweet.ts | 8 +-- src/requests/User.ts | 6 +- src/services/internal/AuthService.ts | 4 +- src/services/internal/ErrorService.ts | 8 +-- src/services/internal/LogService.ts | 4 +- src/services/internal/TidService.ts | 22 +++---- src/services/public/FetcherService.ts | 54 ++++++++-------- src/services/public/ListService.ts | 12 ++-- src/services/public/TweetService.ts | 78 +++++++++++------------ src/services/public/UserService.ts | 80 ++++++++++++------------ src/types/args/FetchArgs.ts | 4 +- src/types/auth/AuthCredential.ts | 4 +- src/types/data/Notification.ts | 4 +- src/types/data/Tweet.ts | 4 +- src/types/raw/base/Media.ts | 4 +- src/types/raw/base/Notification.ts | 4 +- 41 files changed, 322 insertions(+), 318 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a8d43c6f..871593f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,38 +22,43 @@ module.exports = { '@typescript-eslint/naming-convention': [ 'warn', { - selector: ['class'], + selector: ['class', 'enum'], format: ['PascalCase'], }, { - selector: 'interface', + selector: ['interface'], format: ['PascalCase'], prefix: ['I'], }, { - selector: 'enum', + selector: ['typeAlias'], format: ['PascalCase'], - prefix: ['E'], }, { - selector: ['variableLike', 'memberLike'], + selector: ['memberLike', 'variableLike'], format: ['camelCase'], + leadingUnderscore: 'allow', }, { - selector: ['variableLike', 'property'], + selector: ['memberLike'], modifiers: ['private'], format: ['camelCase'], leadingUnderscore: 'require', }, { - selector: ['variableLike', 'memberLike'], + selector: ['memberLike'], modifiers: ['static', 'readonly'], format: ['UPPER_CASE'], }, { - selector: 'enumMember', + selector: ['enumMember'], format: ['UPPER_CASE'], }, + { + selector: ['variable'], + modifiers: ['global'], + format: ['PascalCase'], + }, ], '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-module-boundary-types': 'error', diff --git a/src/cli.ts b/src/cli.ts index 381843d4..b1482b21 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,33 +8,32 @@ import user from './commands/User'; import { Rettiwt } from './Rettiwt'; // Creating a new commandline program -const program = createCommand('rettiwt') +const Program = createCommand('rettiwt') .description('A CLI tool for accessing the Twitter API for free!') .passThroughOptions() .enablePositionalOptions(); // Adding options -program - .option('-k, --key ', 'The API key to use for authentication') +Program.option('-k, --key ', 'The API key to use for authentication') .option('-l, --log', 'Enable logging to console') .option('-p, --proxy ', 'The URL to the proxy to use') .option('-t, --timeout ', 'The timout (in milli-seconds) to use for requests'); // Parsing the program to get supplied options -program.parse(); +Program.parse(); // Initializing Rettiwt instance using the given options -const rettiwt: Rettiwt = new Rettiwt({ - apiKey: process.env.API_KEY ?? (program.opts().key as string), - logging: program.opts().log ? true : false, - proxyUrl: program.opts().proxy as URL, - timeout: program.opts().timeout ? Number(program.opts().timeout) : undefined, +const RettiwtInstance = new Rettiwt({ + apiKey: process.env.API_KEY ?? (Program.opts().key as string), + logging: Program.opts().log ? true : false, + proxyUrl: Program.opts().proxy as URL, + timeout: Program.opts().timeout ? Number(Program.opts().timeout) : undefined, }); // Adding sub-commands -program.addCommand(list(rettiwt)); -program.addCommand(tweet(rettiwt)); -program.addCommand(user(rettiwt)); +Program.addCommand(list(RettiwtInstance)); +Program.addCommand(tweet(RettiwtInstance)); +Program.addCommand(user(RettiwtInstance)); // Finalizing the CLI -program.parse(); +Program.parse(); diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index b4e0fd32..67cc5c24 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,4 +1,4 @@ -import { EBaseType } from '../enums/Data'; +import { BaseType } from '../enums/Data'; import { CursoredData } from '../models/data/CursoredData'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; @@ -43,13 +43,13 @@ import { IUserUnfollowResponse } from '../types/raw/user/Unfollow'; * * @internal */ -export const extractors = { +export const Extractors = { /* eslint-disable @typescript-eslint/naming-convention */ LIST_MEMBERS: (response: IListMembersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), LIST_TWEETS: (response: IListTweetsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), MEDIA_UPLOAD_APPEND: (): void => undefined, MEDIA_UPLOAD_FINALIZE: (): void => undefined, @@ -61,17 +61,17 @@ export const extractors = { TWEET_DETAILS_BULK: (response: ITweetDetailsBulkResponse, ids: string[]): Tweet[] => Tweet.multiple(response, ids), TWEET_LIKE: (response: ITweetLikeResponse): boolean => (response?.data?.favorite_tweet ? true : false), TWEET_LIKERS: (response: ITweetLikersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), TWEET_POST: (response: ITweetPostResponse): string => response?.data?.create_tweet?.tweet_results?.result?.rest_id ?? undefined, TWEET_REPLIES: (response: ITweetDetailsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), TWEET_RETWEET: (response: ITweetRetweetResponse): boolean => (response?.data?.create_retweet ? true : false), TWEET_RETWEETERS: (response: ITweetRetweetersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), TWEET_SCHEDULE: (response: ITweetScheduleResponse): string => response?.data?.tweet?.rest_id ?? undefined, TWEET_SEARCH: (response: ITweetSearchResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), TWEET_UNLIKE: (response: ITweetUnlikeResponse): boolean => (response?.data?.unfavorite_tweet ? true : false), TWEET_UNPOST: (response: ITweetUnpostResponse): boolean => (response?.data?.delete_tweet ? true : false), TWEET_UNRETWEET: (response: ITweetUnretweetResponse): boolean => @@ -79,36 +79,36 @@ export const extractors = { TWEET_UNSCHEDULE: (response: ITweetUnscheduleResponse): boolean => response?.data?.scheduledtweet_delete == 'Done', USER_AFFILIATES: (response: IUserAffiliatesResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_BOOKMARKS: (response: IUserBookmarksResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_DETAILS_BY_USERNAME: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_ID: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_IDS_BULK: (response: IUserDetailsBulkResponse, ids: string[]): User[] => User.multiple(response, ids), USER_FEED_FOLLOWED: (response: IUserFollowedResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_FEED_RECOMMENDED: (response: IUserRecommendedResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_FOLLOW: (response: IUserFollowResponse): boolean => (response?.id ? true : false), USER_FOLLOWING: (response: IUserFollowingResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_FOLLOWERS: (response: IUserFollowersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_HIGHLIGHTS: (response: IUserHighlightsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_LIKES: (response: IUserLikesResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_MEDIA: (response: IUserMediaResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_NOTIFICATIONS: (response: IUserNotificationsResponse): CursoredData => - new CursoredData(response, EBaseType.NOTIFICATION), + new CursoredData(response, BaseType.NOTIFICATION), USER_SUBSCRIPTIONS: (response: IUserSubscriptionsResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_TIMELINE: (response: IUserTweetsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_TIMELINE_AND_REPLIES: (response: IUserTweetsAndRepliesResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_UNFOLLOW: (response: IUserUnfollowResponse): boolean => (response?.id ? true : false), /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index d7962064..bae4b568 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -1,14 +1,14 @@ -import { EResourceType } from '../enums/Resource'; +import { ResourceType } from '../enums/Resource'; /** * Collection of resources that allow guest authentication. * * @internal */ -export const allowGuestAuthentication = [ - EResourceType.TWEET_DETAILS, - EResourceType.USER_DETAILS_BY_USERNAME, - EResourceType.USER_TIMELINE, +export const AllowGuestAuthenticationGroup = [ + ResourceType.TWEET_DETAILS, + ResourceType.USER_DETAILS_BY_USERNAME, + ResourceType.USER_TIMELINE, ]; /** @@ -16,32 +16,32 @@ export const allowGuestAuthentication = [ * * @internal */ -export const fetchResources = [ - EResourceType.LIST_MEMBERS, - EResourceType.LIST_TWEETS, - EResourceType.TWEET_DETAILS, - EResourceType.TWEET_DETAILS_ALT, - EResourceType.TWEET_DETAILS_BULK, - EResourceType.TWEET_LIKERS, - EResourceType.TWEET_REPLIES, - EResourceType.TWEET_RETWEETERS, - EResourceType.TWEET_SEARCH, - EResourceType.USER_AFFILIATES, - EResourceType.USER_BOOKMARKS, - EResourceType.USER_DETAILS_BY_USERNAME, - EResourceType.USER_DETAILS_BY_ID, - EResourceType.USER_DETAILS_BY_IDS_BULK, - EResourceType.USER_FEED_FOLLOWED, - EResourceType.USER_FEED_RECOMMENDED, - EResourceType.USER_FOLLOWING, - EResourceType.USER_FOLLOWERS, - EResourceType.USER_HIGHLIGHTS, - EResourceType.USER_LIKES, - EResourceType.USER_MEDIA, - EResourceType.USER_NOTIFICATIONS, - EResourceType.USER_SUBSCRIPTIONS, - EResourceType.USER_TIMELINE, - EResourceType.USER_TIMELINE_AND_REPLIES, +export const FetchResourcesGroup = [ + ResourceType.LIST_MEMBERS, + ResourceType.LIST_TWEETS, + ResourceType.TWEET_DETAILS, + ResourceType.TWEET_DETAILS_ALT, + ResourceType.TWEET_DETAILS_BULK, + ResourceType.TWEET_LIKERS, + ResourceType.TWEET_REPLIES, + ResourceType.TWEET_RETWEETERS, + ResourceType.TWEET_SEARCH, + ResourceType.USER_AFFILIATES, + ResourceType.USER_BOOKMARKS, + ResourceType.USER_DETAILS_BY_USERNAME, + ResourceType.USER_DETAILS_BY_ID, + ResourceType.USER_DETAILS_BY_IDS_BULK, + ResourceType.USER_FEED_FOLLOWED, + ResourceType.USER_FEED_RECOMMENDED, + ResourceType.USER_FOLLOWING, + ResourceType.USER_FOLLOWERS, + ResourceType.USER_HIGHLIGHTS, + ResourceType.USER_LIKES, + ResourceType.USER_MEDIA, + ResourceType.USER_NOTIFICATIONS, + ResourceType.USER_SUBSCRIPTIONS, + ResourceType.USER_TIMELINE, + ResourceType.USER_TIMELINE_AND_REPLIES, ]; /** @@ -49,18 +49,18 @@ export const fetchResources = [ * * @internal */ -export const postResources = [ - EResourceType.MEDIA_UPLOAD_APPEND, - EResourceType.MEDIA_UPLOAD_FINALIZE, - EResourceType.MEDIA_UPLOAD_INITIALIZE, - EResourceType.TWEET_LIKE, - EResourceType.TWEET_POST, - EResourceType.TWEET_RETWEET, - EResourceType.TWEET_SCHEDULE, - EResourceType.TWEET_UNLIKE, - EResourceType.TWEET_UNPOST, - EResourceType.TWEET_UNRETWEET, - EResourceType.TWEET_UNSCHEDULE, - EResourceType.USER_FOLLOW, - EResourceType.USER_UNFOLLOW, +export const PostResourcesGroup = [ + ResourceType.MEDIA_UPLOAD_APPEND, + ResourceType.MEDIA_UPLOAD_FINALIZE, + ResourceType.MEDIA_UPLOAD_INITIALIZE, + ResourceType.TWEET_LIKE, + ResourceType.TWEET_POST, + ResourceType.TWEET_RETWEET, + ResourceType.TWEET_SCHEDULE, + ResourceType.TWEET_UNLIKE, + ResourceType.TWEET_UNPOST, + ResourceType.TWEET_UNRETWEET, + ResourceType.TWEET_UNSCHEDULE, + ResourceType.USER_FOLLOW, + ResourceType.USER_UNFOLLOW, ]; diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 0f7e1879..6bedeb4d 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -1,6 +1,6 @@ import { AxiosRequestConfig } from 'axios'; -import { EResourceType } from '../enums/Resource'; +import { ResourceType } from '../enums/Resource'; import { ListRequests } from '../requests/List'; import { MediaRequests } from '../requests/Media'; import { TweetRequests } from '../requests/Tweet'; @@ -8,14 +8,14 @@ import { UserRequests } from '../requests/User'; import { IFetchArgs } from '../types/args/FetchArgs'; import { IPostArgs } from '../types/args/PostArgs'; -import { rawTweetRepliesSortType } from './Tweet'; +import { TweetRepliesSortTypeMap } from './Tweet'; /** * Collection of requests to various resources. * * @internal */ -export const requests: { [key in keyof typeof EResourceType]: (args: IFetchArgs | IPostArgs) => AxiosRequestConfig } = { +export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | IPostArgs) => AxiosRequestConfig } = { /* eslint-disable @typescript-eslint/naming-convention */ LIST_MEMBERS: (args: IFetchArgs) => ListRequests.members(args.id!, args.count, args.cursor), @@ -32,7 +32,7 @@ export const requests: { [key in keyof typeof EResourceType]: (args: IFetchArgs TWEET_LIKERS: (args: IFetchArgs) => TweetRequests.likers(args.id!, args.count, args.cursor), TWEET_POST: (args: IPostArgs) => TweetRequests.post(args.tweet!), TWEET_REPLIES: (args: IFetchArgs) => - TweetRequests.replies(args.id!, args.cursor, args.sortBy ? rawTweetRepliesSortType[args.sortBy] : undefined), + TweetRequests.replies(args.id!, args.cursor, args.sortBy ? TweetRepliesSortTypeMap[args.sortBy] : undefined), TWEET_RETWEET: (args: IPostArgs) => TweetRequests.retweet(args.id!), TWEET_RETWEETERS: (args: IFetchArgs) => TweetRequests.retweeters(args.id!, args.count, args.cursor), TWEET_SCHEDULE: (args: IPostArgs) => TweetRequests.schedule(args.tweet!), diff --git a/src/collections/Tweet.ts b/src/collections/Tweet.ts index 3f24bce8..35e9e4c8 100644 --- a/src/collections/Tweet.ts +++ b/src/collections/Tweet.ts @@ -1,17 +1,17 @@ -import { ERawTweetRepliesSortType } from '../enums/raw/Tweet'; -import { ETweetRepliesSortType } from '../enums/Tweet'; +import { RawTweetRepliesSortType } from '../enums/raw/Tweet'; +import { TweetRepliesSortType } from '../enums/Tweet'; /** * Collection of mapping from parsed reply sort type to raw reply sort type. * * @internal */ -export const rawTweetRepliesSortType: { [key in keyof typeof ETweetRepliesSortType]: ERawTweetRepliesSortType } = { +export const TweetRepliesSortTypeMap: { [key in keyof typeof TweetRepliesSortType]: RawTweetRepliesSortType } = { /* eslint-disable @typescript-eslint/naming-convention */ - LATEST: ERawTweetRepliesSortType.LATEST, - LIKES: ERawTweetRepliesSortType.LIKES, - RELEVANCE: ERawTweetRepliesSortType.RELEVACE, + LATEST: RawTweetRepliesSortType.LATEST, + LIKES: RawTweetRepliesSortType.LIKES, + RELEVANCE: RawTweetRepliesSortType.RELEVACE, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/enums/Api.ts b/src/enums/Api.ts index 1dcc56c2..94003c0e 100644 --- a/src/enums/Api.ts +++ b/src/enums/Api.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EApiErrors { +export enum ApiErrors { COULD_NOT_AUTHENTICATE = 'Failed to authenticate', BAD_AUTHENTICATION = 'Invalid authentication data', RESOURCE_NOT_ALLOWED = 'Not authorized to access requested resource', diff --git a/src/enums/Authentication.ts b/src/enums/Authentication.ts index b091a8f9..402ce2b2 100644 --- a/src/enums/Authentication.ts +++ b/src/enums/Authentication.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EAuthenticationType { +export enum AuthenticationType { GUEST = 'GUEST', USER = 'USER', LOGIN = 'LOGIN', diff --git a/src/enums/Data.ts b/src/enums/Data.ts index 2718a33f..195a230e 100644 --- a/src/enums/Data.ts +++ b/src/enums/Data.ts @@ -3,7 +3,7 @@ * * @internal */ -export enum EBaseType { +export enum BaseType { NOTIFICATION = 'NOTIFICATION', TWEET = 'TWEET', USER = 'USER', diff --git a/src/enums/Logging.ts b/src/enums/Logging.ts index 34746fb8..cf537f7c 100644 --- a/src/enums/Logging.ts +++ b/src/enums/Logging.ts @@ -3,7 +3,7 @@ * * @internal */ -export enum ELogActions { +export enum LogActions { AUTHORIZATION = 'AUTHORIZATION', DESERIALIZE = 'DESERIALIZE', EXTRACT = 'EXTRACT', diff --git a/src/enums/Media.ts b/src/enums/Media.ts index 982e7192..38198855 100644 --- a/src/enums/Media.ts +++ b/src/enums/Media.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EMediaType { +export enum MediaType { PHOTO = 'PHOTO', VIDEO = 'VIDEO', GIF = 'GIF', diff --git a/src/enums/Notification.ts b/src/enums/Notification.ts index ca59ea41..754fc63a 100644 --- a/src/enums/Notification.ts +++ b/src/enums/Notification.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ENotificationType { +export enum NotificationType { RECOMMENDATION = 'RECOMMENDATION', INFORMATION = 'INFORMATION', LIVE = 'LIVE', diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 7b7c75c3..4976e9c3 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EResourceType { +export enum ResourceType { // LIST LIST_MEMBERS = 'LIST_MEMBERS', LIST_TWEETS = 'LIST_TWEETS', diff --git a/src/enums/Tweet.ts b/src/enums/Tweet.ts index 195dd0b2..7ebf0353 100644 --- a/src/enums/Tweet.ts +++ b/src/enums/Tweet.ts @@ -1,7 +1,7 @@ /** * The different types of sorting options when fetching replies to tweets. */ -export enum ETweetRepliesSortType { +export enum TweetRepliesSortType { LIKES = 'LIKES', LATEST = 'LATEST', RELEVANCE = 'RELEVANCE', diff --git a/src/enums/raw/Analytics.ts b/src/enums/raw/Analytics.ts index a21fee26..c49ce2e7 100644 --- a/src/enums/raw/Analytics.ts +++ b/src/enums/raw/Analytics.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawAnalyticsGranularity { +export enum RawAnalyticsGranularity { DAILY = 'Daily', WEEKLY = 'Weekly', MONTHLY = 'Monthly', @@ -14,7 +14,7 @@ export enum ERawAnalyticsGranularity { * * @public */ -export enum ERawAnalyticsMetric { +export enum RawAnalyticsMetric { ENGAGEMENTS = 'Engagements', IMPRESSIONS = 'Impressions', PROFILE_VISITS = 'ProfileVisits', diff --git a/src/enums/raw/Media.ts b/src/enums/raw/Media.ts index 65cf0417..0abb4393 100644 --- a/src/enums/raw/Media.ts +++ b/src/enums/raw/Media.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawMediaType { +export enum RawMediaType { PHOTO = 'photo', VIDEO = 'video', GIF = 'animated_gif', diff --git a/src/enums/raw/Notification.ts b/src/enums/raw/Notification.ts index 0723419f..47973fcd 100644 --- a/src/enums/raw/Notification.ts +++ b/src/enums/raw/Notification.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawNotificationType { +export enum RawNotificationType { RECOMMENDATION = 'recommendation_icon', INFORMATION = 'bird_icon', LIVE = 'live_icon', diff --git a/src/enums/raw/Tweet.ts b/src/enums/raw/Tweet.ts index 13a6dcbe..9aa7a06b 100644 --- a/src/enums/raw/Tweet.ts +++ b/src/enums/raw/Tweet.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawTweetSearchResultType { +export enum RawTweetSearchResultType { LATEST = 'Latest', TOP = 'Top', } @@ -13,7 +13,7 @@ export enum ERawTweetSearchResultType { * * @public */ -export enum ERawTweetRepliesSortType { +export enum RawTweetRepliesSortType { RELEVACE = 'Relevance', LATEST = 'Recency', LIKES = 'Likes', diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index e8e982d0..0751051b 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -12,7 +12,7 @@ import { IRettiwtConfig } from '../types/RettiwtConfig'; * * @public */ -const defaultHeaders = { +const DefaultHeaders = { /* eslint-disable @typescript-eslint/naming-convention */ Authority: 'x.com', @@ -60,7 +60,7 @@ export class RettiwtConfig implements IRettiwtConfig { this.timeout = config?.timeout; this.apiKey = config?.apiKey; this._headers = { - ...defaultHeaders, + ...DefaultHeaders, ...config?.headers, }; } @@ -90,7 +90,7 @@ export class RettiwtConfig implements IRettiwtConfig { public set headers(headers: { [key: string]: string } | undefined) { this._headers = { - ...defaultHeaders, + ...DefaultHeaders, ...headers, }; } @@ -100,4 +100,4 @@ export class RettiwtConfig implements IRettiwtConfig { } } -export { defaultHeaders as DefaultRettiwtHeaders }; +export { DefaultHeaders as DefaultRettiwtHeaders }; diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index 741d2234..e8af2209 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -1,4 +1,4 @@ -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { TweetRepliesSortType } from '../../enums/Tweet'; import { IFetchArgs, ITweetFilter } from '../../types/args/FetchArgs'; /** @@ -12,7 +12,7 @@ export class FetchArgs implements IFetchArgs { public filter?: TweetFilter; public id?: string; public ids?: string[]; - public sortBy?: ETweetRepliesSortType; + public sortBy?: TweetRepliesSortType; /** * @param args - Additional user-defined arguments for fetching the resource. @@ -93,7 +93,7 @@ export class TweetFilter implements ITweetFilter { * @param date - The date object to convert. * @returns The Twitter string representation of the date. */ - private static dateToTwitterString(date: Date): string { + private static _dateToTwitterString(date: Date): string { // Converting localized date to UTC date const utc = new Date( Date.UTC( @@ -135,8 +135,8 @@ export class TweetFilter implements ITweetFilter { this.minLikes ? `min_faves:${this.minLikes}` : '', this.minRetweets ? `min_retweets:${this.minRetweets}` : '', this.language ? `lang:${this.language}` : '', - this.startDate ? `since:${TweetFilter.dateToTwitterString(this.startDate)}` : '', - this.endDate ? `until:${TweetFilter.dateToTwitterString(this.endDate)}` : '', + this.startDate ? `since:${TweetFilter._dateToTwitterString(this.startDate)}` : '', + this.endDate ? `until:${TweetFilter._dateToTwitterString(this.endDate)}` : '', this.sinceId ? `since_id:${this.sinceId}` : '', this.maxId ? `max_id:${this.maxId}` : '', this.quoted ? `quoted_tweet_id:${this.quoted}` : '', diff --git a/src/models/auth/AuthCredential.ts b/src/models/auth/AuthCredential.ts index 4fd28f37..9b45185f 100644 --- a/src/models/auth/AuthCredential.ts +++ b/src/models/auth/AuthCredential.ts @@ -2,7 +2,7 @@ import { AxiosHeaders, AxiosRequestHeaders } from 'axios'; import { Cookie } from 'cookiejar'; -import { EAuthenticationType } from '../../enums/Authentication'; +import { AuthenticationType } from '../../enums/Authentication'; import { IAuthCredential } from '../../types/auth/AuthCredential'; import { AuthCookie } from './AuthCookie'; @@ -19,7 +19,7 @@ import { AuthCookie } from './AuthCookie'; */ export class AuthCredential implements IAuthCredential { public authToken?: string; - public authenticationType?: EAuthenticationType; + public authenticationType?: AuthenticationType; public cookies?: string; public csrfToken?: string; public guestToken?: string; @@ -34,7 +34,7 @@ export class AuthCredential implements IAuthCredential { // If guest credentials given if (!cookies && guestToken) { this.guestToken = guestToken; - this.authenticationType = EAuthenticationType.GUEST; + this.authenticationType = AuthenticationType.GUEST; } // If login credentials given else if (cookies && guestToken) { @@ -43,7 +43,7 @@ export class AuthCredential implements IAuthCredential { this.cookies = parsedCookie.toString(); this.guestToken = guestToken; - this.authenticationType = EAuthenticationType.LOGIN; + this.authenticationType = AuthenticationType.LOGIN; } // If user credentials given else if (cookies && !guestToken) { @@ -52,7 +52,7 @@ export class AuthCredential implements IAuthCredential { this.cookies = parsedCookie.toString(); this.csrfToken = parsedCookie.ct0; - this.authenticationType = EAuthenticationType.USER; + this.authenticationType = AuthenticationType.USER; } } diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index 391c605e..b70bfd7f 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -1,4 +1,4 @@ -import { EBaseType } from '../../enums/Data'; +import { BaseType } from '../../enums/Data'; import { findByFilter } from '../../helper/JsonUtils'; @@ -24,18 +24,18 @@ export class CursoredData implements ICur * @param response - The raw response. * @param type - The base type of the data included in the batch. */ - public constructor(response: NonNullable, type: EBaseType) { + public constructor(response: NonNullable, type: BaseType) { // Initializing defaults this.list = []; this.next = ''; - if (type == EBaseType.TWEET) { + if (type == BaseType.TWEET) { this.list = Tweet.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; - } else if (type == EBaseType.USER) { + } else if (type == BaseType.USER) { this.list = User.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; - } else if (type == EBaseType.NOTIFICATION) { + } else if (type == BaseType.NOTIFICATION) { this.list = Notification.list(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Top')[0]?.value ?? ''; } diff --git a/src/models/data/Notification.ts b/src/models/data/Notification.ts index d685f53e..9a00ad51 100644 --- a/src/models/data/Notification.ts +++ b/src/models/data/Notification.ts @@ -1,5 +1,5 @@ -import { ENotificationType } from '../../enums/Notification'; -import { ERawNotificationType } from '../../enums/raw/Notification'; +import { NotificationType } from '../../enums/Notification'; +import { RawNotificationType } from '../../enums/raw/Notification'; import { findKeyByValue } from '../../helper/JsonUtils'; import { INotification } from '../../types/data/Notification'; import { INotification as IRawNotification } from '../../types/raw/base/Notification'; @@ -19,7 +19,7 @@ export class Notification implements INotification { public message: string; public receivedAt: string; public target: string[]; - public type?: ENotificationType; + public type?: NotificationType; /** * @param notification - The raw notification details. @@ -28,7 +28,7 @@ export class Notification implements INotification { this._raw = { ...notification }; // Getting the original notification type - const notificationType: string | undefined = findKeyByValue(ERawNotificationType, notification.icon.id); + const notificationType: string | undefined = findKeyByValue(RawNotificationType, notification.icon.id); this.from = notification.template?.aggregateUserActionsV1?.fromUsers ? notification.template.aggregateUserActionsV1.fromUsers.map((item) => item.user.id) @@ -40,8 +40,8 @@ export class Notification implements INotification { ? notification.template.aggregateUserActionsV1.targetObjects.map((item) => item.tweet.id) : []; this.type = notificationType - ? ENotificationType[notificationType as keyof typeof ENotificationType] - : ENotificationType.UNDEFINED; + ? NotificationType[notificationType as keyof typeof NotificationType] + : NotificationType.UNDEFINED; } /** The raw notification details. */ diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index 9b5eb2e8..a5d7023c 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -1,6 +1,6 @@ -import { ELogActions } from '../../enums/Logging'; -import { EMediaType } from '../../enums/Media'; -import { ERawMediaType } from '../../enums/raw/Media'; +import { LogActions } from '../../enums/Logging'; +import { MediaType } from '../../enums/Media'; +import { RawMediaType } from '../../enums/raw/Media'; import { findByFilter } from '../../helper/JsonUtils'; import { LogService } from '../../services/internal/LogService'; @@ -52,7 +52,7 @@ export class Tweet implements ITweet { this.tweetBy = new User(tweet.core.user_results.result); this.entities = new TweetEntities(tweet.legacy.entities); this.media = tweet.legacy.extended_entities?.media?.map((media) => new TweetMedia(media)); - this.quoted = this.getQuotedTweet(tweet); + this.quoted = this._getQuotedTweet(tweet); this.fullText = tweet.note_tweet ? tweet.note_tweet.note_tweet_results.result.text : tweet.legacy.full_text; this.replyTo = tweet.legacy.in_reply_to_status_id_str; this.lang = tweet.legacy.lang; @@ -62,7 +62,7 @@ export class Tweet implements ITweet { this.likeCount = tweet.legacy.favorite_count; this.viewCount = tweet.views.count ? parseInt(tweet.views.count) : 0; this.bookmarkCount = tweet.legacy.bookmark_count; - this.retweetedTweet = this.getRetweetedTweet(tweet); + this.retweetedTweet = this._getRetweetedTweet(tweet); this.url = `https://x.com/${this.tweetBy.userName}/status/${this.id}`; } @@ -78,7 +78,7 @@ export class Tweet implements ITweet { * * @returns - The deserialized original quoted tweet. */ - private getQuotedTweet(tweet: IRawTweet): Tweet | undefined { + private _getQuotedTweet(tweet: IRawTweet): Tweet | undefined { // If tweet with limited visibility if ( tweet.quoted_status_result && @@ -104,7 +104,7 @@ export class Tweet implements ITweet { * * @returns - The deserialized original retweeted tweet. */ - private getRetweetedTweet(tweet: IRawTweet): Tweet | undefined { + private _getRetweetedTweet(tweet: IRawTweet): Tweet | undefined { // If retweet with limited visibility if ( tweet.legacy?.retweeted_status_result && @@ -141,13 +141,13 @@ export class Tweet implements ITweet { for (const item of extract) { if (item.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); tweets.push(new Tweet(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -179,13 +179,13 @@ export class Tweet implements ITweet { for (const item of extract) { if (item.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); tweets.push(new Tweet(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -221,15 +221,15 @@ export class Tweet implements ITweet { // If normal tweet else if ((item.tweet_results?.result as IRawTweet)?.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: (item.tweet_results.result as IRawTweet).rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: (item.tweet_results.result as IRawTweet).rest_id }); tweets.push(new Tweet(item.tweet_results.result as IRawTweet)); } // If invalid/unrecognized tweet else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -328,7 +328,7 @@ export class TweetMedia { public thumbnailUrl?: string; /** The type of media. */ - public type: EMediaType; + public type: MediaType; /** The direct URL to the media. */ public url = ''; @@ -338,18 +338,18 @@ export class TweetMedia { */ public constructor(media: IRawExtendedMedia) { // If the media is a photo - if (media.type == ERawMediaType.PHOTO) { - this.type = EMediaType.PHOTO; + if (media.type == RawMediaType.PHOTO) { + this.type = MediaType.PHOTO; this.url = media.media_url_https; } // If the media is a gif - else if (media.type == ERawMediaType.GIF) { - this.type = EMediaType.GIF; + else if (media.type == RawMediaType.GIF) { + this.type = MediaType.GIF; this.url = media.video_info?.variants[0].url as string; } // If the media is a video else { - this.type = EMediaType.VIDEO; + this.type = MediaType.VIDEO; this.thumbnailUrl = media.media_url_https; /** The highest bitrate of all variants. */ diff --git a/src/models/data/User.ts b/src/models/data/User.ts index 05b32719..f0029c98 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -1,4 +1,4 @@ -import { ELogActions } from '../../enums/Logging'; +import { LogActions } from '../../enums/Logging'; import { findByFilter } from '../../helper/JsonUtils'; import { LogService } from '../../services/internal/LogService'; import { IUser } from '../../types/data/User'; @@ -73,13 +73,13 @@ export class User implements IUser { for (const item of extract) { if (item.legacy && item.legacy.created_at) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); users.push(new User(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } @@ -110,13 +110,13 @@ export class User implements IUser { for (const item of extract) { if (item.legacy && item.legacy.created_at) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); users.push(new User(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } @@ -142,13 +142,13 @@ export class User implements IUser { for (const item of extract) { if (item.user_results?.result?.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.user_results.result.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.user_results.result.rest_id }); users.push(new User(item.user_results.result)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } diff --git a/src/requests/Tweet.ts b/src/requests/Tweet.ts index d24c53cf..1280bf87 100644 --- a/src/requests/Tweet.ts +++ b/src/requests/Tweet.ts @@ -1,6 +1,6 @@ import { AxiosRequestConfig } from 'axios'; -import { ERawTweetRepliesSortType, ERawTweetSearchResultType } from '../enums/raw/Tweet'; +import { RawTweetRepliesSortType, RawTweetSearchResultType } from '../enums/raw/Tweet'; import { TweetFilter } from '../models/args/FetchArgs'; import { NewTweet } from '../models/args/PostArgs'; import { MediaVariable, ReplyVariable } from '../models/params/Variables'; @@ -266,7 +266,7 @@ export class TweetRequests { * @param id - The id of the tweet whose replies are to be fetched. * @param cursor - The cursor to the batch of replies to fetch. */ - public static replies(id: string, cursor?: string, sortBy?: ERawTweetRepliesSortType): AxiosRequestConfig { + public static replies(id: string, cursor?: string, sortBy?: RawTweetRepliesSortType): AxiosRequestConfig { return { method: 'get', url: 'https://x.com/i/api/graphql/_8aYOgEDz35BrBcBal1-_w/TweetDetail', @@ -277,7 +277,7 @@ export class TweetRequests { cursor: cursor, referrer: 'tweet', with_rux_injections: false, - rankingMode: sortBy ?? ERawTweetRepliesSortType.RELEVACE, + rankingMode: sortBy ?? RawTweetRepliesSortType.RELEVACE, includePromotedContent: true, withCommunity: true, withQuickPromoteEligibilityTweetFields: true, @@ -451,7 +451,7 @@ export class TweetRequests { count: count, cursor: cursor, querySource: 'typed_query', - product: parsedFilter.top ? ERawTweetSearchResultType.TOP : ERawTweetSearchResultType.LATEST, + product: parsedFilter.top ? RawTweetSearchResultType.TOP : RawTweetSearchResultType.LATEST, withAuxiliaryUserLabels: false, withArticleRichContentState: false, withArticlePlainText: false, diff --git a/src/requests/User.ts b/src/requests/User.ts index aa42247a..6ed7d339 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -2,7 +2,7 @@ import qs from 'querystring'; import { AxiosRequestConfig } from 'axios'; -import { ERawAnalyticsGranularity, ERawAnalyticsMetric } from '../enums/raw/Analytics'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics'; /** * Collection of requests related to users. @@ -77,8 +77,8 @@ export class UserRequests { public static analytics( fromTime: Date, toTime: Date, - granularity: ERawAnalyticsGranularity, - requestedMetrics: ERawAnalyticsMetric[], + granularity: RawAnalyticsGranularity, + requestedMetrics: RawAnalyticsMetric[], ): AxiosRequestConfig { return { method: 'get', diff --git a/src/services/internal/AuthService.ts b/src/services/internal/AuthService.ts index f4fd0253..4770c442 100644 --- a/src/services/internal/AuthService.ts +++ b/src/services/internal/AuthService.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import { EApiErrors } from '../../enums/Api'; +import { ApiErrors } from '../../enums/Api'; import { AuthCredential } from '../../models/auth/AuthCredential'; import { RettiwtConfig } from '../../models/RettiwtConfig'; @@ -67,7 +67,7 @@ export class AuthService { } // If user id was not found else { - throw new Error(EApiErrors.BAD_AUTHENTICATION); + throw new Error(ApiErrors.BAD_AUTHENTICATION); } } diff --git a/src/services/internal/ErrorService.ts b/src/services/internal/ErrorService.ts index eb8677a8..ef33f231 100644 --- a/src/services/internal/ErrorService.ts +++ b/src/services/internal/ErrorService.ts @@ -15,14 +15,14 @@ export class ErrorService implements IErrorHandler { * * @param error - The error response received from Twitter. */ - private handleAxiosError(error: AxiosError): void { + private _handleAxiosError(error: AxiosError): void { throw new TwitterError(error); } /** * Handle unknown error. */ - private handleUnknownError(): void { + private _handleUnknownError(): void { throw new Error('Unknown error'); } @@ -33,9 +33,9 @@ export class ErrorService implements IErrorHandler { */ public handle(error: unknown): void { if (isAxiosError(error)) { - this.handleAxiosError(error as AxiosError); + this._handleAxiosError(error as AxiosError); } else { - this.handleUnknownError(); + this._handleUnknownError(); } } } diff --git a/src/services/internal/LogService.ts b/src/services/internal/LogService.ts index e81225f1..090474d7 100644 --- a/src/services/internal/LogService.ts +++ b/src/services/internal/LogService.ts @@ -1,4 +1,4 @@ -import { ELogActions } from '../../enums/Logging'; +import { LogActions } from '../../enums/Logging'; /** * Handles logging of data for debug purpose. @@ -16,7 +16,7 @@ export class LogService { * * @param data - The data to be logged. */ - public static log(action: ELogActions, data: NonNullable): void { + public static log(action: LogActions, data: NonNullable): void { // Proceed to log only if logging is enabled if (this.enabled) { // Preparing the log message diff --git a/src/services/internal/TidService.ts b/src/services/internal/TidService.ts index 1ad1d19d..7215bac1 100644 --- a/src/services/internal/TidService.ts +++ b/src/services/internal/TidService.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import * as htmlParser from 'node-html-parser'; -import { ELogActions } from '../../enums/Logging'; +import { LogActions } from '../../enums/Logging'; import { calculateClientTransactionIdHeader } from '../../helper/TidUtils'; @@ -34,16 +34,16 @@ export class TidService implements ITidProvider { * * @returns The new dynamic args. */ - private async getDynamicArgs(): Promise { - const html = await this.getHomepageHtml(); + private async _getDynamicArgs(): Promise { + const html = await this._getHomepageHtml(); const root = htmlParser.parse(html); const keyElement = root.querySelector("[name='twitter-site-verification']"); const frameElements = root.querySelectorAll("[id^='loading-x-anim']"); return { verificationKey: keyElement?.getAttribute('content') ?? '', - frames: frameElements.map((el) => this.parseFrameElement(el)), - indices: await this.getKeyBytesIndices(html), + frames: frameElements.map((el) => this._parseFrameElement(el)), + indices: await this._getKeyBytesIndices(html), }; } @@ -52,7 +52,7 @@ export class TidService implements ITidProvider { * * @returns The stringified HTML content of the homepage. */ - private async getHomepageHtml(): Promise { + private async _getHomepageHtml(): Promise { const response = await axios.get('https://x.com', { headers: this._config.headers, httpAgent: this._config.httpsAgent, @@ -62,10 +62,10 @@ export class TidService implements ITidProvider { return response.data; } - private async getKeyBytesIndices(html: string): Promise { + private async _getKeyBytesIndices(html: string): Promise { const ondemandFileMatch = html.match(/ondemand\.s":"([^"]+)"/); if (!ondemandFileMatch || !ondemandFileMatch[1]) { - LogService.log(ELogActions.WARNING, { message: 'ondemand.s file not found' }); + LogService.log(LogActions.WARNING, { message: 'ondemand.s file not found' }); return [0, 0, 0, 0]; } @@ -80,7 +80,7 @@ export class TidService implements ITidProvider { return Array.from(match).map((m) => Number(m[2])); } - private parseFrameElement(element: htmlParser.HTMLElement): number[][] { + private _parseFrameElement(element: htmlParser.HTMLElement): number[][] { const pathElement = element.children[0].children[1]; const value = pathElement.getAttribute('d'); if (!value) { @@ -122,7 +122,7 @@ export class TidService implements ITidProvider { extraByte: 3, }); } catch (err) { - LogService.log(ELogActions.WARNING, { + LogService.log(LogActions.WARNING, { message: 'Failed to generated transaction token. Request may or may not work', error: err, }); @@ -135,6 +135,6 @@ export class TidService implements ITidProvider { * Refreshes the dynamic args from the homepage. */ public async refreshDynamicArgs(): Promise { - this._dynamicArgs = await this.getDynamicArgs(); + this._dynamicArgs = await this._getDynamicArgs(); } } diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 08aa0deb..8c19fffd 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,11 +1,11 @@ import axios from 'axios'; import { Cookie } from 'cookiejar'; -import { allowGuestAuthentication, fetchResources, postResources } from '../../collections/Groups'; -import { requests } from '../../collections/Requests'; -import { EApiErrors } from '../../enums/Api'; -import { ELogActions } from '../../enums/Logging'; -import { EResourceType } from '../../enums/Resource'; +import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; +import { Requests } from '../../collections/Requests'; +import { ApiErrors } from '../../enums/Api'; +import { LogActions } from '../../enums/Logging'; +import { ResourceType } from '../../enums/Resource'; import { FetchArgs } from '../../models/args/FetchArgs'; import { PostArgs } from '../../models/args/PostArgs'; import { AuthCredential } from '../../models/auth/AuthCredential'; @@ -65,13 +65,13 @@ export class FetcherService { * * @throws An error if not authorized to access the requested resource. */ - private checkAuthorization(resource: EResourceType): void { + private _checkAuthorization(resource: ResourceType): void { // Logging - LogService.log(ELogActions.AUTHORIZATION, { authenticated: this.config.userId != undefined }); + LogService.log(LogActions.AUTHORIZATION, { authenticated: this.config.userId != undefined }); // Checking authorization status - if (!allowGuestAuthentication.includes(resource) && this.config.userId == undefined) { - throw new Error(EApiErrors.RESOURCE_NOT_ALLOWED); + if (!AllowGuestAuthenticationGroup.includes(resource) && this.config.userId == undefined) { + throw new Error(ApiErrors.RESOURCE_NOT_ALLOWED); } } @@ -80,10 +80,10 @@ export class FetcherService { * * @returns The generated AuthCredential */ - private async getCredential(): Promise { + private async _getCredential(): Promise { if (this.config.apiKey) { // Logging - LogService.log(ELogActions.GET, { target: 'USER_CREDENTIAL' }); + LogService.log(LogActions.GET, { target: 'USER_CREDENTIAL' }); return new AuthCredential( AuthService.decodeCookie(this.config.apiKey) @@ -92,7 +92,7 @@ export class FetcherService { ); } else { // Logging - LogService.log(ELogActions.GET, { target: 'NEW_GUEST_CREDENTIAL' }); + LogService.log(LogActions.GET, { target: 'NEW_GUEST_CREDENTIAL' }); return this._auth.guest(); } @@ -106,7 +106,7 @@ export class FetcherService { * * @returns The header containing the transaction ID. */ - private async getTransactionHeader(method: string, url: string): Promise { + private async _getTransactionHeader(method: string, url: string): Promise { // Getting the URL path excluding all params const path = new URL(url).pathname.split('?')[0].trim(); @@ -132,15 +132,15 @@ export class FetcherService { * * @returns The validated args. */ - private validateArgs(resource: EResourceType, args: IFetchArgs | IPostArgs): FetchArgs | PostArgs | undefined { - if (fetchResources.includes(resource)) { + private _validateArgs(resource: ResourceType, args: IFetchArgs | IPostArgs): FetchArgs | PostArgs | undefined { + if (FetchResourcesGroup.includes(resource)) { // Logging - LogService.log(ELogActions.VALIDATE, { target: 'FETCH_ARGS' }); + LogService.log(LogActions.VALIDATE, { target: 'FETCH_ARGS' }); return new FetchArgs(args); - } else if (postResources.includes(resource)) { + } else if (PostResourcesGroup.includes(resource)) { // Logging - LogService.log(ELogActions.VALIDATE, { target: 'POST_ARGS' }); + LogService.log(LogActions.VALIDATE, { target: 'POST_ARGS' }); return new PostArgs(args); } @@ -149,7 +149,7 @@ export class FetcherService { /** * Introduces a delay using the configured delay/delay function. */ - private async wait(): Promise { + private async _wait(): Promise { // If no delay is set, skip if (this._delay == undefined) { return; @@ -198,27 +198,27 @@ export class FetcherService { * }); * ``` */ - public async request(resource: EResourceType, args: IFetchArgs | IPostArgs): Promise { + public async request(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise { // Logging - LogService.log(ELogActions.REQUEST, { resource: resource, args: args }); + LogService.log(LogActions.REQUEST, { resource: resource, args: args }); // Checking authorization for the requested resource - this.checkAuthorization(resource); + this._checkAuthorization(resource); // Validating args - args = this.validateArgs(resource, args)!; + args = this._validateArgs(resource, args)!; // Getting credentials from key - const cred: AuthCredential = await this.getCredential(); + const cred: AuthCredential = await this._getCredential(); // Getting request configuration - const config = requests[resource](args); + const config = Requests[resource](args); // Setting additional request parameters config.headers = { ...config.headers, ...cred.toHeader(), - ...(await this.getTransactionHeader(config.method ?? '', config.url ?? '')), + ...(await this._getTransactionHeader(config.method ?? '', config.url ?? '')), ...this.config.headers, }; config.httpAgent = this.config.httpsAgent; @@ -228,7 +228,7 @@ export class FetcherService { // Sending the request try { // Introducing a delay - await this.wait(); + await this._wait(); // Returning the reponse body return (await axios(config)).data; diff --git a/src/services/public/ListService.ts b/src/services/public/ListService.ts index a178fd5f..47d3277e 100644 --- a/src/services/public/ListService.ts +++ b/src/services/public/ListService.ts @@ -1,5 +1,5 @@ -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; import { CursoredData } from '../../models/data/CursoredData'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; @@ -49,7 +49,7 @@ export class ListService extends FetcherService { * @remarks Due a bug in Twitter API, the count is ignored when no cursor is provided and defaults to 100. */ public async members(id: string, count?: number, cursor?: string): Promise> { - const resource: EResourceType = EResourceType.LIST_MEMBERS; + const resource: ResourceType = ResourceType.LIST_MEMBERS; // Fetching the raw list of members const response = await this.request(resource, { @@ -59,7 +59,7 @@ export class ListService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -94,7 +94,7 @@ export class ListService extends FetcherService { * @remarks Due a bug in Twitter API, the count is ignored when no cursor is provided and defaults to 100. */ public async tweets(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.LIST_TWEETS; + const resource = ResourceType.LIST_TWEETS; // Fetching raw list tweets const response = await this.request(resource, { @@ -104,7 +104,7 @@ export class ListService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); // Sorting the tweets by date, from recent to oldest data.list.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); diff --git a/src/services/public/TweetService.ts b/src/services/public/TweetService.ts index 6b2e4034..1327429a 100644 --- a/src/services/public/TweetService.ts +++ b/src/services/public/TweetService.ts @@ -1,8 +1,8 @@ import { statSync } from 'fs'; -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; +import { TweetRepliesSortType } from '../../enums/Tweet'; import { CursoredData } from '../../models/data/CursoredData'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; @@ -95,41 +95,41 @@ export class TweetService extends FetcherService { * ``` */ public async details(id: T): Promise { - let resource: EResourceType; + let resource: ResourceType; // If user is authenticated and details of single tweet required if (this.config.userId != undefined && typeof id == 'string') { - resource = EResourceType.TWEET_DETAILS_ALT; + resource = ResourceType.TWEET_DETAILS_ALT; // Fetching raw tweet details const response = await this.request(resource, { id: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); return data as T extends string ? Tweet | undefined : Tweet[]; } // If user is authenticated and details of multiple tweets required else if (this.config.userId != undefined && Array.isArray(id)) { - resource = EResourceType.TWEET_DETAILS_BULK; + resource = ResourceType.TWEET_DETAILS_BULK; // Fetching raw tweet details const response = await this.request(resource, { ids: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); return data as T extends string ? Tweet | undefined : Tweet[]; } // If user is not authenticated else { - resource = EResourceType.TWEET_DETAILS; + resource = ResourceType.TWEET_DETAILS; // Fetching raw tweet details const response = await this.request(resource, { id: String(id) }); // Deserializing response - const data = extractors[resource](response, String(id)); + const data = Extractors[resource](response, String(id)); return data as T extends string ? Tweet | undefined : Tweet[]; } @@ -161,7 +161,7 @@ export class TweetService extends FetcherService { * ``` */ public async like(id: string): Promise { - const resource = EResourceType.TWEET_LIKE; + const resource = ResourceType.TWEET_LIKE; // Favoriting the tweet const response = await this.request(resource, { @@ -169,7 +169,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -202,7 +202,7 @@ export class TweetService extends FetcherService { * ``` */ public async likers(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_LIKERS; + const resource = ResourceType.TWEET_LIKERS; // Fetching raw likers const response = await this.request(resource, { @@ -212,7 +212,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -301,13 +301,13 @@ export class TweetService extends FetcherService { * ``` */ public async post(options: INewTweet): Promise { - const resource = EResourceType.TWEET_POST; + const resource = ResourceType.TWEET_POST; // Posting the tweet const response = await this.request(resource, { tweet: options }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -317,7 +317,7 @@ export class TweetService extends FetcherService { * * @param id - The ID of the target tweet. * @param cursor - The cursor to the batch of replies to fetch. - * @param sortBy - The sorting order of the replies to fetch. Default is {@link ETweetRepliesSortType.RECENT}. + * @param sortBy - The sorting order of the replies to fetch. Default is {@link TweetRepliesSortType.RECENT}. * * @returns The list of replies to the given tweet. * @@ -346,9 +346,9 @@ export class TweetService extends FetcherService { public async replies( id: string, cursor?: string, - sortBy: ETweetRepliesSortType = ETweetRepliesSortType.LATEST, + sortBy: TweetRepliesSortType = TweetRepliesSortType.LATEST, ): Promise> { - const resource = EResourceType.TWEET_REPLIES; + const resource = ResourceType.TWEET_REPLIES; // Fetching raw list of replies const response = await this.request(resource, { @@ -358,7 +358,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -389,13 +389,13 @@ export class TweetService extends FetcherService { * ``` */ public async retweet(id: string): Promise { - const resource = EResourceType.TWEET_RETWEET; + const resource = ResourceType.TWEET_RETWEET; // Retweeting the tweet const response = await this.request(resource, { id: id }); // Deserializing response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -428,7 +428,7 @@ export class TweetService extends FetcherService { * ``` */ public async retweeters(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_RETWEETERS; + const resource = ResourceType.TWEET_RETWEETERS; // Fetching raw list of retweeters const response = await this.request(resource, { @@ -438,7 +438,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -474,13 +474,13 @@ export class TweetService extends FetcherService { * Scheduling a tweet is similar to {@link post}ing, except that an extra parameter called `scheduleFor` is used. */ public async schedule(options: INewTweet): Promise { - const resource = EResourceType.TWEET_SCHEDULE; + const resource = ResourceType.TWEET_SCHEDULE; // Scheduling the tweet const response = await this.request(resource, { tweet: options }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -518,7 +518,7 @@ export class TweetService extends FetcherService { * For details about available filters, refer to {@link TweetFilter} */ public async search(filter: ITweetFilter, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_SEARCH; + const resource = ResourceType.TWEET_SEARCH; // Fetching raw list of filtered tweets const response = await this.request(resource, { @@ -528,7 +528,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); // Sorting the tweets by date, from recent to oldest data.list.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); @@ -631,13 +631,13 @@ export class TweetService extends FetcherService { * ``` */ public async unlike(id: string): Promise { - const resource = EResourceType.TWEET_UNLIKE; + const resource = ResourceType.TWEET_UNLIKE; // Unliking the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -668,13 +668,13 @@ export class TweetService extends FetcherService { * ``` */ public async unpost(id: string): Promise { - const resource = EResourceType.TWEET_UNPOST; + const resource = ResourceType.TWEET_UNPOST; // Unposting the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -705,13 +705,13 @@ export class TweetService extends FetcherService { * ``` */ public async unretweet(id: string): Promise { - const resource = EResourceType.TWEET_UNRETWEET; + const resource = ResourceType.TWEET_UNRETWEET; // Unretweeting the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -742,13 +742,13 @@ export class TweetService extends FetcherService { * ``` */ public async unschedule(id: string): Promise { - const resource = EResourceType.TWEET_UNSCHEDULE; + const resource = ResourceType.TWEET_UNSCHEDULE; // Unscheduling the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -788,16 +788,16 @@ export class TweetService extends FetcherService { // INITIALIZE const size = typeof media == 'string' ? statSync(media).size : media.byteLength; const id: string = ( - await this.request(EResourceType.MEDIA_UPLOAD_INITIALIZE, { + await this.request(ResourceType.MEDIA_UPLOAD_INITIALIZE, { upload: { size: size }, }) ).media_id_string; // APPEND - await this.request(EResourceType.MEDIA_UPLOAD_APPEND, { upload: { id: id, media: media } }); + await this.request(ResourceType.MEDIA_UPLOAD_APPEND, { upload: { id: id, media: media } }); // FINALIZE - await this.request(EResourceType.MEDIA_UPLOAD_FINALIZE, { upload: { id: id } }); + await this.request(ResourceType.MEDIA_UPLOAD_FINALIZE, { upload: { id: id } }); return id; } diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 36097304..d94a34b0 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -1,5 +1,5 @@ -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; import { CursoredData } from '../../models/data/CursoredData'; import { Notification } from '../../models/data/Notification'; import { Tweet } from '../../models/data/Tweet'; @@ -68,7 +68,7 @@ export class UserService extends FetcherService { * ``` */ public async affiliates(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_AFFILIATES; + const resource = ResourceType.USER_AFFILIATES; // Fetching raw list of affiliates const response = await this.request(resource, { @@ -78,7 +78,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -110,7 +110,7 @@ export class UserService extends FetcherService { * ``` */ public async bookmarks(count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_BOOKMARKS; + const resource = ResourceType.USER_BOOKMARKS; // Fetching raw list of likes const response = await this.request(resource, { @@ -119,7 +119,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -195,17 +195,17 @@ export class UserService extends FetcherService { public async details( id: T, ): Promise { - let resource: EResourceType; + let resource: ResourceType; // If details of multiple users required if (Array.isArray(id)) { - resource = EResourceType.USER_DETAILS_BY_IDS_BULK; + resource = ResourceType.USER_DETAILS_BY_IDS_BULK; // Fetching raw details const response = await this.request(resource, { ids: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); return data as T extends string | undefined ? User | undefined : User[]; } @@ -213,11 +213,11 @@ export class UserService extends FetcherService { else { // If username is given if (id && isNaN(Number(id))) { - resource = EResourceType.USER_DETAILS_BY_USERNAME; + resource = ResourceType.USER_DETAILS_BY_USERNAME; } // If id is given (or not, for self details) else { - resource = EResourceType.USER_DETAILS_BY_ID; + resource = ResourceType.USER_DETAILS_BY_ID; } // If no ID is given, and not authenticated, skip @@ -229,7 +229,7 @@ export class UserService extends FetcherService { const response = await this.request(resource, { id: id ?? this.config.userId }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data as T extends string | undefined ? User | undefined : User[]; } @@ -263,13 +263,13 @@ export class UserService extends FetcherService { * ``` */ public async follow(id: string): Promise { - const resource = EResourceType.USER_FOLLOW; + const resource = ResourceType.USER_FOLLOW; // Following the user - const response = await this.request(EResourceType.USER_FOLLOW, { id: id }); + const response = await this.request(ResourceType.USER_FOLLOW, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -302,7 +302,7 @@ export class UserService extends FetcherService { * @remarks Always returns 35 feed items, with no way to customize the count. */ public async followed(cursor?: string): Promise> { - const resource = EResourceType.USER_FEED_FOLLOWED; + const resource = ResourceType.USER_FEED_FOLLOWED; // Fetching raw list of tweets const response = await this.request(resource, { @@ -310,7 +310,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -343,7 +343,7 @@ export class UserService extends FetcherService { * ``` */ public async followers(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_FOLLOWERS; + const resource = ResourceType.USER_FOLLOWERS; // Fetching raw list of followers const response = await this.request(resource, { @@ -353,7 +353,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -386,7 +386,7 @@ export class UserService extends FetcherService { * ``` */ public async following(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_FOLLOWING; + const resource = ResourceType.USER_FOLLOWING; // Fetching raw list of following const response = await this.request(resource, { @@ -396,7 +396,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -429,7 +429,7 @@ export class UserService extends FetcherService { * ``` */ public async highlights(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_HIGHLIGHTS; + const resource = ResourceType.USER_HIGHLIGHTS; // Fetching raw list of highlights const response = await this.request(resource, { @@ -439,7 +439,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -471,7 +471,7 @@ export class UserService extends FetcherService { * ``` */ public async likes(count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_LIKES; + const resource = ResourceType.USER_LIKES; // Fetching raw list of likes const response = await this.request(resource, { @@ -481,7 +481,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -514,7 +514,7 @@ export class UserService extends FetcherService { * ``` */ public async media(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_MEDIA; + const resource = ResourceType.USER_MEDIA; // Fetching raw list of media const response = await this.request(resource, { @@ -524,7 +524,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -562,7 +562,7 @@ export class UserService extends FetcherService { * ``` */ public async *notifications(pollingInterval = 60000): AsyncGenerator { - const resource = EResourceType.USER_NOTIFICATIONS; + const resource = ResourceType.USER_NOTIFICATIONS; /** Whether it's the first batch of notifications or not. */ let first = true; @@ -581,7 +581,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const notifications = extractors[resource](response); + const notifications = Extractors[resource](response); // Sorting the notifications by time, from oldest to recent notifications.list.sort((a, b) => new Date(a.receivedAt).valueOf() - new Date(b.receivedAt).valueOf()); @@ -630,7 +630,7 @@ export class UserService extends FetcherService { * @remarks Always returns 35 feed items, with no way to customize the count. */ public async recommended(cursor?: string): Promise> { - const resource = EResourceType.USER_FEED_RECOMMENDED; + const resource = ResourceType.USER_FEED_RECOMMENDED; // Fetching raw list of tweets const response = await this.request(resource, { @@ -638,7 +638,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -675,7 +675,7 @@ export class UserService extends FetcherService { * If the target user has a pinned tweet, the returned reply timeline has one item extra and this is always the pinned tweet. */ public async replies(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_TIMELINE_AND_REPLIES; + const resource = ResourceType.USER_TIMELINE_AND_REPLIES; // Fetching raw list of replies const response = await this.request(resource, { @@ -685,7 +685,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -720,7 +720,7 @@ export class UserService extends FetcherService { * ``` */ public async subscriptions(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_SUBSCRIPTIONS; + const resource = ResourceType.USER_SUBSCRIPTIONS; // Fetching raw list of subscriptions const response = await this.request(resource, { @@ -730,7 +730,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -768,7 +768,7 @@ export class UserService extends FetcherService { * - If timeline is fetched without authenticating, then the most popular tweets of the target user are returned instead. */ public async timeline(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_TIMELINE; + const resource = ResourceType.USER_TIMELINE; // Fetching raw list of tweets const response = await this.request(resource, { @@ -778,7 +778,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -809,13 +809,13 @@ export class UserService extends FetcherService { * ``` */ public async unfollow(id: string): Promise { - const resource = EResourceType.USER_UNFOLLOW; + const resource = ResourceType.USER_UNFOLLOW; // Unfollowing the user - const response = await this.request(EResourceType.USER_UNFOLLOW, { id: id }); + const response = await this.request(ResourceType.USER_UNFOLLOW, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index 8c101a51..ddc78773 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -1,4 +1,4 @@ -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { TweetRepliesSortType } from '../../enums/Tweet'; /** * Options specifying the data that is to be fetched. @@ -64,7 +64,7 @@ export interface IFetchArgs { * @remarks * - Only works for {@link EResourceType.TWEET_REPLIES}. */ - sortBy?: ETweetRepliesSortType; + sortBy?: TweetRepliesSortType; } /** diff --git a/src/types/auth/AuthCredential.ts b/src/types/auth/AuthCredential.ts index 7db29fad..580fe5e8 100644 --- a/src/types/auth/AuthCredential.ts +++ b/src/types/auth/AuthCredential.ts @@ -1,4 +1,4 @@ -import { EAuthenticationType } from '../../enums/Authentication'; +import { AuthenticationType } from '../../enums/Authentication'; /** * The credentials for authenticating against Twitter. @@ -15,7 +15,7 @@ export interface IAuthCredential { authToken?: string; /** The type of authentication. */ - authenticationType?: EAuthenticationType; + authenticationType?: AuthenticationType; /** The cookie of the twitter account, which is used to authenticate against twitter. */ cookies?: string; diff --git a/src/types/data/Notification.ts b/src/types/data/Notification.ts index 262b6576..221867b1 100644 --- a/src/types/data/Notification.ts +++ b/src/types/data/Notification.ts @@ -1,4 +1,4 @@ -import { ENotificationType } from '../../enums/Notification'; +import { NotificationType } from '../../enums/Notification'; /** * The details of a single notification. @@ -22,5 +22,5 @@ export interface INotification { target: string[]; /** The type of notification. */ - type?: ENotificationType; + type?: NotificationType; } diff --git a/src/types/data/Tweet.ts b/src/types/data/Tweet.ts index 87b0d44f..47081a39 100644 --- a/src/types/data/Tweet.ts +++ b/src/types/data/Tweet.ts @@ -1,4 +1,4 @@ -import { EMediaType } from '../../enums/Media'; +import { MediaType } from '../../enums/Media'; import { IUser } from './User'; @@ -89,7 +89,7 @@ export interface ITweetMedia { thumbnailUrl?: string; /** The type of media. */ - type: EMediaType; + type: MediaType; /** The direct URL to the media. */ url: string; diff --git a/src/types/raw/base/Media.ts b/src/types/raw/base/Media.ts index ae353774..25793c8b 100644 --- a/src/types/raw/base/Media.ts +++ b/src/types/raw/base/Media.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -import { ERawMediaType } from '../../../enums/raw/Media'; +import { RawMediaType } from '../../../enums/raw/Media'; /** * Represents the raw data of a single Media. @@ -12,7 +12,7 @@ export interface IMedia { expanded_url: string; id_str: string; media_url_https: string; - type: ERawMediaType; + type: RawMediaType; url: string; } diff --git a/src/types/raw/base/Notification.ts b/src/types/raw/base/Notification.ts index 7a87bb72..a3ceb04e 100644 --- a/src/types/raw/base/Notification.ts +++ b/src/types/raw/base/Notification.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -import { ERawNotificationType } from '../../../enums/raw/Notification'; +import { RawNotificationType } from '../../../enums/raw/Notification'; /** * Represents the raw data of a single Notification. @@ -16,7 +16,7 @@ export interface INotification { } export interface INotificationIcon { - id: ERawNotificationType; + id: RawNotificationType; } export interface INotificationMessage { From 6ae56bba778197d04a37381469f735c1ab63dfc3 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Mon, 26 May 2025 21:00:48 +0530 Subject: [PATCH 002/119] Added ability to fetch replies to a tweet via CLI --- src/commands/Tweet.ts | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/commands/Tweet.ts b/src/commands/Tweet.ts index 907c35e7..ca2b9d8c 100644 --- a/src/commands/Tweet.ts +++ b/src/commands/Tweet.ts @@ -1,5 +1,6 @@ import { Command, createCommand } from 'commander'; +import { TweetRepliesSortType } from '../enums/Tweet'; import { output } from '../helper/CliUtils'; import { TweetFilter } from '../models/args/FetchArgs'; import { Rettiwt } from '../Rettiwt'; @@ -63,8 +64,8 @@ function createTweetCommand(rettiwt: Rettiwt): Command { .argument('[cursor]', 'The cursor to the batch of likers to fetch') .action(async (id: string, count?: string, cursor?: string) => { try { - const tweets = await rettiwt.tweet.likers(id, count ? parseInt(count) : undefined, cursor); - output(tweets); + const users = await rettiwt.tweet.likers(id, count ? parseInt(count) : undefined, cursor); + output(users); } catch (error) { output(error); } @@ -95,6 +96,34 @@ function createTweetCommand(rettiwt: Rettiwt): Command { } }); + // Replies + tweet + .command('replies') + .description( + 'Fetch the list of replies to a tweet, with the first batch containing the whole thread, if the tweet is/part of a thread', + ) + .argument('', 'The id of the tweet') + .argument('[cursor]', 'The cursor to the batch of replies to fetch') + .option('-s, --sort-by ', 'Sort the tweets by likes, latest or relevance, default is latest') + .action(async (id: string, cursor?: string, options?: { sortBy: string }) => { + try { + // Determining the sort type + let sortType: TweetRepliesSortType | undefined = undefined; + if (options?.sortBy === 'likes') { + sortType = TweetRepliesSortType.LIKES; + } else if (options?.sortBy === 'latest') { + sortType = TweetRepliesSortType.LATEST; + } else if (options?.sortBy === 'relevance') { + sortType = TweetRepliesSortType.RELEVANCE; + } + + const tweets = await rettiwt.tweet.replies(id, cursor, sortType); + output(tweets); + } catch (error) { + output(error); + } + }); + // Retweet tweet .command('retweet') @@ -118,8 +147,8 @@ function createTweetCommand(rettiwt: Rettiwt): Command { .argument('[cursor]', 'The cursor to the batch of retweeters to fetch') .action(async (id: string, count?: string, cursor?: string) => { try { - const tweets = await rettiwt.tweet.retweeters(id, count ? parseInt(count) : undefined, cursor); - output(tweets); + const users = await rettiwt.tweet.retweeters(id, count ? parseInt(count) : undefined, cursor); + output(users); } catch (error) { output(error); } From be84f53f94028aebb3efffd381d96575ff80830a Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 27 May 2025 19:52:59 +0530 Subject: [PATCH 003/119] Updated RettiwtConfig to include max retries parameter --- src/models/RettiwtConfig.ts | 2 ++ src/types/RettiwtConfig.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index 0751051b..eb65508a 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -43,6 +43,7 @@ export class RettiwtConfig implements IRettiwtConfig { public readonly delay?: number | (() => number | Promise); public readonly errorHandler?: IErrorHandler; public readonly logging?: boolean; + public readonly maxRetries?: number; public readonly tidProvider?: ITidProvider; public readonly timeout?: number; @@ -54,6 +55,7 @@ export class RettiwtConfig implements IRettiwtConfig { this._httpsAgent = config?.proxyUrl ? new HttpsProxyAgent(config?.proxyUrl) : new Agent(); this._userId = config?.apiKey ? AuthService.getUserId(config?.apiKey) : undefined; this.delay = config?.delay; + this.maxRetries = config?.maxRetries; this.errorHandler = config?.errorHandler; this.logging = config?.logging; this.tidProvider = config?.tidProvider; diff --git a/src/types/RettiwtConfig.ts b/src/types/RettiwtConfig.ts index f39eda6e..56e6a139 100644 --- a/src/types/RettiwtConfig.ts +++ b/src/types/RettiwtConfig.ts @@ -42,4 +42,7 @@ export interface IRettiwtConfig { * Can either be a number or a function that returns a number synchronously or asynchronously. */ delay?: number | (() => number | Promise); + + /** The maximum number of retries to use. */ + maxRetries?: number; } From ce3f273d84b869a2cb27baf10d3d35220985af45 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 27 May 2025 20:30:49 +0530 Subject: [PATCH 004/119] Added ability to use a maximum set number of retries --- src/models/RettiwtConfig.ts | 4 +-- src/services/public/FetcherService.ts | 40 +++++++++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index eb65508a..0490df4c 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -43,7 +43,7 @@ export class RettiwtConfig implements IRettiwtConfig { public readonly delay?: number | (() => number | Promise); public readonly errorHandler?: IErrorHandler; public readonly logging?: boolean; - public readonly maxRetries?: number; + public readonly maxRetries: number; public readonly tidProvider?: ITidProvider; public readonly timeout?: number; @@ -55,7 +55,7 @@ export class RettiwtConfig implements IRettiwtConfig { this._httpsAgent = config?.proxyUrl ? new HttpsProxyAgent(config?.proxyUrl) : new Agent(); this._userId = config?.apiKey ? AuthService.getUserId(config?.apiKey) : undefined; this.delay = config?.delay; - this.maxRetries = config?.maxRetries; + this.maxRetries = config?.maxRetries ?? 1; this.errorHandler = config?.errorHandler; this.logging = config?.logging; this.tidProvider = config?.tidProvider; diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 8c19fffd..249b2934 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; @@ -199,6 +199,9 @@ export class FetcherService { * ``` */ public async request(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise { + /** The error, if any. */ + let error: unknown = undefined; + // Logging LogService.log(LogActions.REQUEST, { resource: resource, args: args }); @@ -225,17 +228,30 @@ export class FetcherService { config.httpsAgent = this.config.httpsAgent; config.timeout = this._timeout; - // Sending the request - try { - // Introducing a delay - await this._wait(); - - // Returning the reponse body - return (await axios(config)).data; - } catch (error) { - // If error, delegate handling to error handler - this._errorHandler.handle(error); - throw error; + // Using retries for error 404 + for (let retry = 1; retry <= this.config.maxRetries; retry++) { + // Sending the request + try { + // Introducing a delay + await this._wait(); + + // Returning the reponse body + return (await axios(config)).data; + } catch (err) { + // If it's an error 404, retry + if (isAxiosError(err) && err.status === 404) { + error = err; + continue; + } + // Else, delegate error handling + else { + this._errorHandler.handle(err); + throw err; + } + } } + + /** If request not successful even after retries, throw the error */ + throw error; } } From 37ab535a9308f419b6097e946af4f0ab54e8e6d2 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 27 May 2025 20:37:01 +0530 Subject: [PATCH 005/119] Updated docs --- src/types/RettiwtConfig.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/RettiwtConfig.ts b/src/types/RettiwtConfig.ts index 56e6a139..35d4be72 100644 --- a/src/types/RettiwtConfig.ts +++ b/src/types/RettiwtConfig.ts @@ -43,6 +43,10 @@ export interface IRettiwtConfig { */ delay?: number | (() => number | Promise); - /** The maximum number of retries to use. */ + /** + * The maximum number of retries to use. + * + * @remarks Recommended to use a value of 5 combined with a `delay` of 1000 to prevent error 404. + */ maxRetries?: number; } From ae0458910d4625622defbba0836b296047e7a4fe Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 27 May 2025 20:44:33 +0530 Subject: [PATCH 006/119] Updated CLI to provide more options --- src/cli.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index b1482b21..83f46818 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,12 @@ const Program = createCommand('rettiwt') Program.option('-k, --key ', 'The API key to use for authentication') .option('-l, --log', 'Enable logging to console') .option('-p, --proxy ', 'The URL to the proxy to use') - .option('-t, --timeout ', 'The timout (in milli-seconds) to use for requests'); + .option('-t, --timeout ', 'The timout (in milli-seconds) to use for requests') + .option( + '-r, --retries ', + 'The maximum number of retries to use, a value of 5 combined with a delay of 1000 is recommended', + ) + .option('-d, --delay ', 'The delay in milliseconds to use in-between successive requests'); // Parsing the program to get supplied options Program.parse(); @@ -28,6 +33,8 @@ const RettiwtInstance = new Rettiwt({ logging: Program.opts().log ? true : false, proxyUrl: Program.opts().proxy as URL, timeout: Program.opts().timeout ? Number(Program.opts().timeout) : undefined, + maxRetries: Program.opts().retries as number, + delay: Program.opts().delay as number, }); // Adding sub-commands From b26209848730d951b52c41037042c7a35c3883ce Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Wed, 4 Jun 2025 15:55:17 +0000 Subject: [PATCH 007/119] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4340f8a9..0daf12da 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ dist # VSCode configs .vscode +.devcontainer # Stores VSCode versions used for testing VSCode extensions .vscode-test From 04c366e24fd59be33266e1f3ea8f3f6183cc634e Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Wed, 4 Jun 2025 16:22:16 +0000 Subject: [PATCH 008/119] Removed .tool-version --- .tool-versions | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index b8867a43..00000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 22.13.1 From 768efa7ade01cec2faf3a8f351761a76544bf608 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Wed, 4 Jun 2025 19:24:52 +0000 Subject: [PATCH 009/119] Updated defaults for delay and retries --- src/models/RettiwtConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index 0490df4c..718921f0 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -54,8 +54,8 @@ export class RettiwtConfig implements IRettiwtConfig { this._apiKey = config?.apiKey; this._httpsAgent = config?.proxyUrl ? new HttpsProxyAgent(config?.proxyUrl) : new Agent(); this._userId = config?.apiKey ? AuthService.getUserId(config?.apiKey) : undefined; - this.delay = config?.delay; - this.maxRetries = config?.maxRetries ?? 1; + this.delay = config?.delay ?? 1000; + this.maxRetries = config?.maxRetries ?? 5; this.errorHandler = config?.errorHandler; this.logging = config?.logging; this.tidProvider = config?.tidProvider; From 862c0e28a34ff7cd62d420195fa78395b5304c6b Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Wed, 11 Jun 2025 16:13:16 +0000 Subject: [PATCH 010/119] Updated some dependencies --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 066ef671..a0a6e9ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -931,9 +931,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { From d27231275f9dfef515e910a3b628f58d1237fd86 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Wed, 11 Jun 2025 16:19:45 +0000 Subject: [PATCH 011/119] Updated packages --- package-lock.json | 589 +++++++++++++++++++++++++++------------------- package.json | 34 +-- 2 files changed, 360 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0a6e9ed..a926a02e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,37 +9,37 @@ "version": "5.1.0-alpha.0", "license": "ISC", "dependencies": { - "axios": "1.8.4", - "chalk": "5.4.1", - "commander": "11.1.0", - "cookiejar": "2.1.4", - "https-proxy-agent": "7.0.6", - "node-html-parser": "7.0.1" + "axios": "^1.8.4", + "chalk": "^5.4.1", + "commander": "^11.1.0", + "cookiejar": "^2.1.4", + "https-proxy-agent": "^7.0.6", + "node-html-parser": "^7.0.1" }, "bin": { "rettiwt": "dist/cli.js" }, "devDependencies": { - "@types/cookiejar": "2.1.5", - "@types/node": "22.13.1", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", - "eslint": "9.20.1", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-tsdoc": "0.4.0", - "nodemon": "3.1.9", - "prettier": "3.5.1", - "typedoc": "0.27.7", - "typescript": "5.7.3" + "@types/cookiejar": "^2.1.5", + "@types/node": "^22.13.1", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-tsdoc": "^0.4.0", + "nodemon": "^3.1.9", + "prettier": "^3.5.1", + "typedoc": "^0.27.7", + "typescript": "^5.7.3" }, "engines": { "node": "^22.13.1" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -66,9 +66,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -81,9 +81,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -104,10 +104,20 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -118,9 +128,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -142,9 +152,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -152,6 +162,16 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -166,13 +186,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -186,32 +209,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@gerrit0/mini-shiki": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", @@ -277,9 +287,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -416,9 +426,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -447,13 +457,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "version": "22.15.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", + "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/unist": { @@ -464,21 +474,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -488,22 +498,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -515,38 +525,77 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -557,13 +606,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -575,20 +624,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -598,20 +649,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -622,17 +673,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -644,9 +695,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -657,9 +708,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -760,18 +811,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -781,18 +834,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -894,9 +948,9 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -986,14 +1040,14 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1214,9 +1268,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1377,9 +1431,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1387,18 +1441,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -1410,21 +1464,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -1433,7 +1490,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -1532,22 +1589,23 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -1555,7 +1613,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -1676,9 +1734,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1731,9 +1789,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1761,9 +1819,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1789,9 +1847,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1811,6 +1869,16 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1838,15 +1906,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1856,9 +1924,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1966,9 +2034,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2033,9 +2101,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -2076,14 +2144,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2146,17 +2215,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -2376,9 +2445,9 @@ } }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2653,6 +2722,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3086,9 +3168,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", "dev": true, "license": "MIT", "dependencies": { @@ -3115,9 +3197,9 @@ } }, "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3399,9 +3481,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -3567,9 +3649,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -3657,9 +3739,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -3830,6 +3912,20 @@ "node": ">=10" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -3962,9 +4058,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -4079,9 +4175,9 @@ } }, "node_modules/typedoc": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.7.tgz", - "integrity": "sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==", + "version": "0.27.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", + "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4098,13 +4194,13 @@ "node": ">= 18" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4149,9 +4245,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -4249,16 +4345,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -4280,16 +4377,16 @@ } }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index 6bfecbd4..475f2a4f 100644 --- a/package.json +++ b/package.json @@ -32,24 +32,24 @@ "node": "^22.13.1" }, "devDependencies": { - "@types/cookiejar": "2.1.5", - "@types/node": "22.13.1", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", - "eslint": "9.20.1", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-tsdoc": "0.4.0", - "nodemon": "3.1.9", - "prettier": "3.5.1", - "typedoc": "0.27.7", - "typescript": "5.7.3" + "@types/cookiejar": "^2.1.5", + "@types/node": "^22.13.1", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-tsdoc": "^0.4.0", + "nodemon": "^3.1.9", + "prettier": "^3.5.1", + "typedoc": "^0.27.7", + "typescript": "^5.7.3" }, "dependencies": { - "axios": "1.8.4", - "chalk": "5.4.1", - "commander": "11.1.0", - "cookiejar": "2.1.4", - "https-proxy-agent": "7.0.6", - "node-html-parser": "7.0.1" + "axios": "^1.8.4", + "chalk": "^5.4.1", + "commander": "^11.1.0", + "cookiejar": "^2.1.4", + "https-proxy-agent": "^7.0.6", + "node-html-parser": "^7.0.1" } } From 6cf506bed1d3341546b6b194ed04d05ddd5333a7 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Fri, 13 Jun 2025 16:32:55 +0000 Subject: [PATCH 012/119] Updated README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8124646a..cc918ce2 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,8 @@ When initializing a new Rettiwt instance, it can be configures using various par - `errorHandler` (interface) - The custom error handler to use. - `tidProvider` (interface) - The custom TID provider to use for generating transaction token. - `headers` (object) - Custom HTTP headers to append to the default headers. -- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. +- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. Default is 1000. +- `maxRetries` (number) - The maximum number of retries to use in case when a random error 404 is encountered. Default is 5. Of these parameters, the following are hot-swappable, using their respective setters: @@ -423,6 +424,12 @@ As demonstrated by the example, the raw data can be accessed by using the `reque - For for hot-swapping in case of using `FetcherService`, the setters are accessed from the `config` object as `config.apiKey = ...`, `config.proxyUrl = ...`, etc. +## Data serialization + +The data returned by all functions of `Rettiwt` are complex objects, containing non-serialized fields like `raw`. In order to get JSON-serializable data, all data objects returned by `Rettiwt` provide a function `toJSON()` which converts the data into a serializable JSON, whose type is described by their respective interfaces i.e, `ITweet` for `Tweet`, `IUser` for `User` and so on. + +For handling and processing of data returned by the functions, it's always advisable to serialize them using the `toJSON()` function. + ## Features So far, the following operations are supported: From 51d3fcbb23027437c0f6db9c339362c85dc06a67 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Fri, 13 Jun 2025 16:33:50 +0000 Subject: [PATCH 013/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a926a02e..6b7e62ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "5.1.0", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 475f2a4f..ebc9bc4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "5.1.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 3c2b58cfe4566b30c8c433c71c1572949730d898 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Fri, 13 Jun 2025 16:44:14 +0000 Subject: [PATCH 014/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b7e62ef..6581919c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "5.1.0", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "5.1.0", + "version": "6.0.0", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index ebc9bc4f..111e4983 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "5.1.0", + "version": "6.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From f543233ab88a79ac1f89792f7abf9b5318e55e8d Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 13:35:10 +0000 Subject: [PATCH 015/119] Used more accurate transaction ID generation --- .npmrc | 1 + package-lock.json | 78 ++++++++++ package.json | 1 + src/helper/TidUtils.ts | 198 -------------------------- src/index.ts | 1 - src/models/RettiwtConfig.ts | 3 - src/services/internal/TidService.ts | 140 ------------------ src/services/public/FetcherService.ts | 36 +++-- src/types/RettiwtConfig.ts | 4 - src/types/auth/TidDynamicArgs.ts | 10 -- src/types/auth/TidHeader.ts | 12 -- src/types/auth/TidParams.ts | 36 ----- src/types/auth/TidProvider.ts | 19 --- src/types/auth/TransactionHeader.ts | 8 ++ 14 files changed, 105 insertions(+), 442 deletions(-) create mode 100644 .npmrc delete mode 100644 src/helper/TidUtils.ts delete mode 100644 src/services/internal/TidService.ts delete mode 100644 src/types/auth/TidDynamicArgs.ts delete mode 100644 src/types/auth/TidHeader.ts delete mode 100644 src/types/auth/TidParams.ts delete mode 100644 src/types/auth/TidProvider.ts create mode 100644 src/types/auth/TransactionHeader.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/package-lock.json b/package-lock.json index 6581919c..54ed526e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "6.0.0", "license": "ISC", "dependencies": { + "@lami/x-client-transaction-id": "npm:@jsr/lami__x-client-transaction-id@^0.1.7", "axios": "^1.8.4", "chalk": "^5.4.1", "commander": "^11.1.0", @@ -300,6 +301,21 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jsr/std__encoding": { + "version": "1.0.10", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", + "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==" + }, + "node_modules/@lami/x-client-transaction-id": { + "name": "@jsr/lami__x-client-transaction-id", + "version": "0.1.7", + "resolved": "https://npm.jsr.io/~/11/@jsr/lami__x-client-transaction-id/0.1.7.tgz", + "integrity": "sha512-B+KfQOuBjFVV8wu7ayhVhW6nKya0g/738BNokiePufu7EsL8ZRwVUXR7gUaGPLijhm49pGikMImQrMgOEBKx6w==", + "dependencies": { + "@jsr/std__encoding": "^1.0.10", + "linkedom": "^0.18.9" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", @@ -1213,6 +1229,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2431,6 +2453,43 @@ "he": "bin/he" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2999,6 +3058,19 @@ "node": ">= 0.8.0" } }, + "node_modules/linkedom": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.11.tgz", + "integrity": "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA==", + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -4218,6 +4290,12 @@ "dev": true, "license": "MIT" }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "license": "ISC" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 111e4983..0dadfb68 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "typescript": "^5.7.3" }, "dependencies": { + "@lami/x-client-transaction-id": "npm:@jsr/lami__x-client-transaction-id@^0.1.7", "axios": "^1.8.4", "chalk": "^5.4.1", "commander": "^11.1.0", diff --git a/src/helper/TidUtils.ts b/src/helper/TidUtils.ts deleted file mode 100644 index 15d3213d..00000000 --- a/src/helper/TidUtils.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { createHash } from 'node:crypto'; - -import { ITidParams } from '../types/auth/TidParams'; - -export function getUnixTime(): number { - const [seconds, nanoseconds] = process.hrtime(); - return Date.now() / 1000 + seconds + nanoseconds / 1e9; -} - -export function getNanosecondPrecisionTime(): number { - return Number(((BigInt(Date.now()) * 1000000n + process.hrtime.bigint()) * 1000n) / BigInt(1000000)) / 1000000; -} - -export function calculateClientTransactionIdHeader(args: ITidParams): string { - const time = Math.floor(((args.time || getUnixTime()) * 1000 - 1682924400 * 1000) / 1000); - const timeBuffer = new Uint8Array(new Uint32Array([time]).buffer); - - const keyBytes = Array.from(Buffer.from(args.verificationKey, 'base64')); - const animationKey = args.animationKey || getAnimationKey(keyBytes, args.frames, args.indices); - - const value = [args.method, args.path, time].join('!') + args.keyword + animationKey; - const valueEncoded = new TextEncoder().encode(value); - - const hash = createHash('sha-256').update(valueEncoded).digest(); - const hashBytes = Array.from(new Uint8Array(hash)); - - const xorByte = args.xorByte || Math.floor(Math.random() * 256); - - const bytes = new Uint8Array(keyBytes.concat(Array.from(timeBuffer), hashBytes.slice(0, 16), [args.extraByte])); - return encode(xor(xorByte, bytes)); -} - -function getAnimationKey(keyBytes: number[], frames: number[][][], indices: number[]): string { - const totalTime = 4096; - const rowIndex = keyBytes[indices[0]] % 16; - const frameTime = indices - .slice(1) - .map((idx) => keyBytes[idx] % 16) - .reduce((a, b) => a * b, 1); - const targetTime = frameTime / totalTime; - const frameRow = frames[keyBytes[5] % 4][rowIndex]; - return animate(frameRow, targetTime); -} - -function animate(frameRow: number[], targetTime: number): string { - const curves = frameRow.slice(7).map((v, i) => Number(a(v, b(i), 1).toFixed(2))); - const cubicValue = getCubicCurveValue(curves, targetTime); - - const fromColor = [...frameRow.slice(0, 3), 1]; - const toColor = [...frameRow.slice(3, 6), 1]; - const color = interpolate(fromColor, toColor, cubicValue); - - const fromRotation = [0]; - const toRotation = [Math.floor(a(frameRow[6], 60, 360))]; - const rotation = interpolate(fromRotation, toRotation, cubicValue); - const matrix = convertRotationToMatrix(rotation[0]); - - const strArray: string[] = []; - for (let i = 0; i < color.length - 1; i++) { - strArray.push(Math.round(color[i]).toString(16)); - } - - for (let i = 0; i < matrix.length; i++) { - let rounded = Number(matrix[i].toFixed(2)); - if (rounded < 0) { - rounded = -rounded; - } - const hexValue = floatToHex(rounded); - if (hexValue.startsWith('.')) { - strArray.push('0' + hexValue); - } else if (hexValue) { - strArray.push(hexValue); - } else { - strArray.push('0'); - } - } - - strArray.push('0', '0'); - return strArray.join('').replace(/[.-]/g, '').toLowerCase(); -} - -function a(b: number, c: number, d: number): number { - return (b * (d - c)) / 255 + c; -} - -function b(a: number): number { - return a % 2 === 1 ? -1 : 0; -} - -function getCubicCurveValue(curves: number[], time: number): number { - let startGradient = 0; - let endGradient = 0; - - if (time <= 0) { - if (curves[0] > 0) { - startGradient = curves[1] / curves[0]; - } else if (curves[1] === 0 && curves[2] > 0) { - startGradient = curves[3] / curves[2]; - } - return startGradient * time; - } - - if (time >= 1) { - if (curves[2] < 1) { - endGradient = (curves[3] - 1) / (curves[2] - 1); - } else if (curves[2] === 1 && curves[0] < 1) { - endGradient = (curves[1] - 1) / (curves[0] - 1); - } - return 1 + endGradient * (time - 1); - } - - let start = 0; - let end = 1; - let mid = 0; - - while (start < end) { - mid = (start + end) / 2; - const xEst = calculateBezier(curves[0], curves[2], mid); - if (Math.abs(time - xEst) < 0.00001) { - return calculateBezier(curves[1], curves[3], mid); - } - if (xEst < time) { - start = mid; - } else { - end = mid; - } - } - - return calculateBezier(curves[1], curves[3], mid); -} - -function calculateBezier(a: number, b: number, m: number): number { - return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m; -} - -function interpolate(from: number[], to: number[], f: number): number[] { - const out: number[] = []; - for (let i = 0; i < from.length; i++) { - out.push(from[i] * (1 - f) + to[i] * f); - } - return out; -} - -function convertRotationToMatrix(degrees: number): number[] { - const radians = (degrees * Math.PI) / 180; - const c = Math.cos(radians); - const s = Math.sin(radians); - return [c, -s, s, c]; -} - -function floatToHex(x: number): string { - const result: string[] = []; - let quotient = Math.floor(x); - let fraction = x - quotient; - - while (quotient > 0) { - const remainder = quotient % 16; - quotient = Math.floor(quotient / 16); - - if (remainder > 9) { - result.unshift(String.fromCharCode(remainder + 55)); - } else { - result.unshift(remainder.toString()); - } - } - - if (fraction === 0) { - return result.join(''); - } - - result.push('.'); - - while (fraction > 0) { - fraction *= 16; - const integer = Math.floor(fraction); - fraction -= integer; - - if (integer > 9) { - result.push(String.fromCharCode(integer + 55)); - } else { - result.push(integer.toString()); - } - } - - return result.join(''); -} - -function xor(xorByte: number, data: Uint8Array): Uint8Array { - return new Uint8Array([xorByte, ...data.map((v) => v ^ xorByte)]); -} - -function encode(data: Uint8Array): string { - return btoa( - Array.from(data) - .map((v) => String.fromCharCode(v)) - .join(''), - ).replaceAll('=', ''); -} diff --git a/src/index.ts b/src/index.ts index d1efbac2..cbb7e0a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,6 @@ export * from './services/public/UserService'; // TYPES export * from './types/args/FetchArgs'; export * from './types/args/PostArgs'; -export * from './types/auth/TidProvider'; export * from './types/data/CursoredData'; export * from './types/data/List'; export * from './types/data/Notification'; diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index 718921f0..a5a21dcc 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -3,7 +3,6 @@ import { Agent } from 'https'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { AuthService } from '../services/internal/AuthService'; -import { ITidProvider } from '../types/auth/TidProvider'; import { IErrorHandler } from '../types/ErrorHandler'; import { IRettiwtConfig } from '../types/RettiwtConfig'; @@ -44,7 +43,6 @@ export class RettiwtConfig implements IRettiwtConfig { public readonly errorHandler?: IErrorHandler; public readonly logging?: boolean; public readonly maxRetries: number; - public readonly tidProvider?: ITidProvider; public readonly timeout?: number; /** @@ -58,7 +56,6 @@ export class RettiwtConfig implements IRettiwtConfig { this.maxRetries = config?.maxRetries ?? 5; this.errorHandler = config?.errorHandler; this.logging = config?.logging; - this.tidProvider = config?.tidProvider; this.timeout = config?.timeout; this.apiKey = config?.apiKey; this._headers = { diff --git a/src/services/internal/TidService.ts b/src/services/internal/TidService.ts deleted file mode 100644 index 7215bac1..00000000 --- a/src/services/internal/TidService.ts +++ /dev/null @@ -1,140 +0,0 @@ -import axios from 'axios'; -import * as htmlParser from 'node-html-parser'; - -import { LogActions } from '../../enums/Logging'; - -import { calculateClientTransactionIdHeader } from '../../helper/TidUtils'; - -import { RettiwtConfig } from '../../models/RettiwtConfig'; -import { ITidDynamicArgs } from '../../types/auth/TidDynamicArgs'; -import { ITidProvider } from '../../types/auth/TidProvider'; - -import { LogService } from './LogService'; - -/** - * Handles transaction ID generation for requests to Twitter. - * - * @internal - */ -export class TidService implements ITidProvider { - private readonly _cdnUrl: string; - private readonly _config: RettiwtConfig; - private _dynamicArgs?: ITidDynamicArgs; - - /** - * @param config - The config for Rettiwt. - */ - public constructor(config: RettiwtConfig) { - this._cdnUrl = 'https://abs.twimg.com/responsive-web/client-web'; - this._config = config; - } - - /** - * Fetches the dynamic args embedded in the homepage. - * - * @returns The new dynamic args. - */ - private async _getDynamicArgs(): Promise { - const html = await this._getHomepageHtml(); - const root = htmlParser.parse(html); - const keyElement = root.querySelector("[name='twitter-site-verification']"); - const frameElements = root.querySelectorAll("[id^='loading-x-anim']"); - - return { - verificationKey: keyElement?.getAttribute('content') ?? '', - frames: frameElements.map((el) => this._parseFrameElement(el)), - indices: await this._getKeyBytesIndices(html), - }; - } - - /** - * Fetches the HTML content of Twitter's homepage. - * - * @returns The stringified HTML content of the homepage. - */ - private async _getHomepageHtml(): Promise { - const response = await axios.get('https://x.com', { - headers: this._config.headers, - httpAgent: this._config.httpsAgent, - httpsAgent: this._config.httpsAgent, - }); - - return response.data; - } - - private async _getKeyBytesIndices(html: string): Promise { - const ondemandFileMatch = html.match(/ondemand\.s":"([^"]+)"/); - if (!ondemandFileMatch || !ondemandFileMatch[1]) { - LogService.log(LogActions.WARNING, { message: 'ondemand.s file not found' }); - - return [0, 0, 0, 0]; - } - - const onDemandFileHash = ondemandFileMatch ? ondemandFileMatch[1] : ''; - const response = await axios.get(`${this._cdnUrl}/ondemand.s.${onDemandFileHash}a.js`, { - httpAgent: this._config.httpsAgent, - httpsAgent: this._config.httpsAgent, - }); - const match = response.data.matchAll(/(\(\w\[(\d{1,2})],\s*16\))+?/gm); - - return Array.from(match).map((m) => Number(m[2])); - } - - private _parseFrameElement(element: htmlParser.HTMLElement): number[][] { - const pathElement = element.children[0].children[1]; - const value = pathElement.getAttribute('d'); - if (!value) { - return [[]]; - } - - const rawFrames = value.substring(9).split('C'); - - return rawFrames.map((str) => str.replaceAll(/\D+/g, ' ').trim().split(' ')).map((arr) => arr.map(Number)); - } - - /** - * Generate an `x-client-transaction-id` for the specific URL method and path. - * - * @param method - The target method. - * @param path - The target path. - * - * @returns The specific `x-client-transaction-id` token. - */ - public async generate(method: string, path: string): Promise { - try { - // Refreshing dynamic args - await this.refreshDynamicArgs(); - - // If dynamic args weren't obtained, skip with error - if (!this._dynamicArgs) { - throw new Error('Dynamic args failed to generate'); - } - - const { verificationKey, frames, indices } = this._dynamicArgs; - - return calculateClientTransactionIdHeader({ - keyword: 'obfiowerehiring', - method: method, - path: path, - verificationKey: verificationKey, - frames: frames, - indices: indices, - extraByte: 3, - }); - } catch (err) { - LogService.log(LogActions.WARNING, { - message: 'Failed to generated transaction token. Request may or may not work', - error: err, - }); - - return; - } - } - - /** - * Refreshes the dynamic args from the homepage. - */ - public async refreshDynamicArgs(): Promise { - this._dynamicArgs = await this._getDynamicArgs(); - } -} diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 249b2934..9d67ee2c 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-unresolved */ +import { ClientTransaction, handleXMigration } from '@lami/x-client-transaction-id'; import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; @@ -12,14 +14,12 @@ import { AuthCredential } from '../../models/auth/AuthCredential'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IFetchArgs } from '../../types/args/FetchArgs'; import { IPostArgs } from '../../types/args/PostArgs'; -import { ITidHeader } from '../../types/auth/TidHeader'; -import { ITidProvider } from '../../types/auth/TidProvider'; +import { ITransactionHeader } from '../../types/auth/TransactionHeader'; import { IErrorHandler } from '../../types/ErrorHandler'; import { AuthService } from '../internal/AuthService'; import { ErrorService } from '../internal/ErrorService'; import { LogService } from '../internal/LogService'; -import { TidService } from '../internal/TidService'; /** * The base service that handles all HTTP requests. @@ -36,9 +36,6 @@ export class FetcherService { /** The service used to handle HTTP and API errors */ private readonly _errorHandler: IErrorHandler; - /** Service responsible for generating the `x-client-transaction-id` header. */ - private readonly _tidProvider: ITidProvider; - /** The max wait time for a response. */ private readonly _timeout: number; @@ -53,7 +50,6 @@ export class FetcherService { this.config = config; this._delay = config.delay; this._errorHandler = config.errorHandler ?? new ErrorService(); - this._tidProvider = config.tidProvider ?? new TidService(config); this._timeout = config.timeout ?? 0; this._auth = new AuthService(config); } @@ -106,22 +102,24 @@ export class FetcherService { * * @returns The header containing the transaction ID. */ - private async _getTransactionHeader(method: string, url: string): Promise { + private async _getTransactionHeader(method: string, url: string): Promise { + // Get the X homepage HTML document (using utility function) + const document = await handleXMigration(); + + // Create and initialize ClientTransaction instance + const transaction = await ClientTransaction.create(document); + // Getting the URL path excluding all params const path = new URL(url).pathname.split('?')[0].trim(); // Generating the transaction ID - const tid = await this._tidProvider.generate(method.toUpperCase(), path); - - if (tid) { - return { - /* eslint-disable @typescript-eslint/naming-convention */ - 'x-client-transaction-id': tid, - /* eslint-enable @typescript-eslint/naming-convention */ - }; - } else { - return undefined; - } + const tid = await transaction.generateTransactionId(method.toUpperCase(), path); + + return { + /* eslint-disable @typescript-eslint/naming-convention */ + 'x-client-transaction-id': tid, + /* eslint-enable @typescript-eslint/naming-convention */ + }; } /** diff --git a/src/types/RettiwtConfig.ts b/src/types/RettiwtConfig.ts index 35d4be72..5b48a91b 100644 --- a/src/types/RettiwtConfig.ts +++ b/src/types/RettiwtConfig.ts @@ -1,4 +1,3 @@ -import { ITidProvider } from './auth/TidProvider'; import { IErrorHandler } from './ErrorHandler'; /** @@ -26,9 +25,6 @@ export interface IRettiwtConfig { /** Optional custom error handler to define error conditions and process API/HTTP errors in responses. */ errorHandler?: IErrorHandler; - /** Optional custom `x-client-transaction-id` header provider. */ - tidProvider?: ITidProvider; - /** * Optional custom HTTP headers to add to all requests to Twitter API. * diff --git a/src/types/auth/TidDynamicArgs.ts b/src/types/auth/TidDynamicArgs.ts deleted file mode 100644 index abdb4538..00000000 --- a/src/types/auth/TidDynamicArgs.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Internal args used for generating trasaction ID. - * - * @internal - */ -export interface ITidDynamicArgs { - verificationKey: string; - frames: number[][][]; - indices: number[]; -} diff --git a/src/types/auth/TidHeader.ts b/src/types/auth/TidHeader.ts deleted file mode 100644 index a724c1f6..00000000 --- a/src/types/auth/TidHeader.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * The header for the transaction ID. - * - * @public - */ -export interface ITidHeader { - /* eslint-disable @typescript-eslint/naming-convention */ - - 'x-client-transaction-id': string; - - /* eslint-enable @typescript-eslint/naming-convention */ -} diff --git a/src/types/auth/TidParams.ts b/src/types/auth/TidParams.ts deleted file mode 100644 index cd25f34c..00000000 --- a/src/types/auth/TidParams.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * The parameters for generating the transaction ID. - * - * @internal - */ -export interface ITidParams { - /** Secret used for transaction ID calculation. */ - keyword: string; - - /** Request method. */ - method: string; - - /** Endpoint path without query parameters. */ - path: string; - - /** Twitter verification key received from HTML. */ - verificationKey: string; - - /** Animation frames extracted from HTML. */ - frames: number[][][]; - - /** Indices used for getting the correct verification key bytes during animation key calculation. */ - indices: number[]; - - /** Final byte of the transaction ID. */ - extraByte: number; - - /** Current time */ - time?: number; - - /** XOR byte used for final hash calculation, must be in 0-255 range. */ - xorByte?: number; - - /** Precomputed animation key. */ - animationKey?: string; -} diff --git a/src/types/auth/TidProvider.ts b/src/types/auth/TidProvider.ts deleted file mode 100644 index 541c9152..00000000 --- a/src/types/auth/TidProvider.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Service responsible for generating the `x-client-transaction-id` header. - * - * @public - */ -export interface ITidProvider { - /** - * Generates new `x-client-transaction-id` header. - * - * @param method - Request method. - * @param path - Endpoint path without query parameters. - */ - generate(method: string, path: string): Promise; - - /** - * Refresh arguments obtained from parsing the HTML page, if any. - */ - refreshDynamicArgs(): Promise; -} diff --git a/src/types/auth/TransactionHeader.ts b/src/types/auth/TransactionHeader.ts new file mode 100644 index 00000000..c75c9a18 --- /dev/null +++ b/src/types/auth/TransactionHeader.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * The transaction information for Twitter. + */ +export interface ITransactionHeader { + 'x-client-transaction-id': string; +} From a619bf620d524c98b52a8bb5dcb28d6940c86306 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 13:40:07 +0000 Subject: [PATCH 016/119] Transaction ID is generated for every retry --- src/services/public/FetcherService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 9d67ee2c..c1cf8503 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -219,7 +219,6 @@ export class FetcherService { config.headers = { ...config.headers, ...cred.toHeader(), - ...(await this._getTransactionHeader(config.method ?? '', config.url ?? '')), ...this.config.headers, }; config.httpAgent = this.config.httpsAgent; @@ -230,6 +229,12 @@ export class FetcherService { for (let retry = 1; retry <= this.config.maxRetries; retry++) { // Sending the request try { + // Getting and appending transaction information + config.headers = { + ...config.headers, + ...(await this._getTransactionHeader(config.method ?? '', config.url ?? '')), + }; + // Introducing a delay await this._wait(); From cddbffbdf7d6d38932b509f4948917d8b55d6341 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 13:45:25 +0000 Subject: [PATCH 017/119] Reset default maxRetries and delay to 0 since it's no longer required for mitigation of error 404 --- src/models/RettiwtConfig.ts | 4 ++-- src/services/public/FetcherService.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index a5a21dcc..ee67178a 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -52,8 +52,8 @@ export class RettiwtConfig implements IRettiwtConfig { this._apiKey = config?.apiKey; this._httpsAgent = config?.proxyUrl ? new HttpsProxyAgent(config?.proxyUrl) : new Agent(); this._userId = config?.apiKey ? AuthService.getUserId(config?.apiKey) : undefined; - this.delay = config?.delay ?? 1000; - this.maxRetries = config?.maxRetries ?? 5; + this.delay = config?.delay ?? 0; + this.maxRetries = config?.maxRetries ?? 0; this.errorHandler = config?.errorHandler; this.logging = config?.logging; this.timeout = config?.timeout; diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index c1cf8503..78288c98 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -197,6 +197,9 @@ export class FetcherService { * ``` */ public async request(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise { + /** The current retry number. */ + let retry = 0; + /** The error, if any. */ let error: unknown = undefined; @@ -226,7 +229,7 @@ export class FetcherService { config.timeout = this._timeout; // Using retries for error 404 - for (let retry = 1; retry <= this.config.maxRetries; retry++) { + do { // Sending the request try { // Getting and appending transaction information @@ -251,8 +254,11 @@ export class FetcherService { this._errorHandler.handle(err); throw err; } + } finally { + // Incrementing the number of retries done + retry++; } - } + } while (retry < this.config.maxRetries); /** If request not successful even after retries, throw the error */ throw error; From 0101c46a175948135fd85b9506f47490d90e311e Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 13:49:27 +0000 Subject: [PATCH 018/119] Updated README.md --- README.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cc918ce2..ff741c50 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,8 @@ When initializing a new Rettiwt instance, it can be configures using various par - `errorHandler` (interface) - The custom error handler to use. - `tidProvider` (interface) - The custom TID provider to use for generating transaction token. - `headers` (object) - Custom HTTP headers to append to the default headers. -- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. Default is 1000. -- `maxRetries` (number) - The maximum number of retries to use in case when a random error 404 is encountered. Default is 5. +- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. Default is 0 (no delay). +- `maxRetries` (number) - The maximum number of retries to use in case when a random error 404 is encountered. Default is 0 (no retries). Of these parameters, the following are hot-swappable, using their respective setters: diff --git a/package-lock.json b/package-lock.json index 54ed526e..04d79e91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.0", + "version": "6.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.0", + "version": "6.0.1", "license": "ISC", "dependencies": { "@lami/x-client-transaction-id": "npm:@jsr/lami__x-client-transaction-id@^0.1.7", diff --git a/package.json b/package.json index 0dadfb68..be7b19b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.0", + "version": "6.0.1", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 1fc8979c8cde57c4396c16dc0660690162542ce8 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 19:52:30 +0000 Subject: [PATCH 019/119] Fixed broken dependency --- .npmrc | 1 - package-lock.json | 28 +++++++++++---------------- package.json | 6 +++--- src/services/public/FetcherService.ts | 3 +-- 4 files changed, 15 insertions(+), 23 deletions(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 41583e36..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@jsr:registry=https://npm.jsr.io diff --git a/package-lock.json b/package-lock.json index 04d79e91..4db9bfdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "6.0.1", "license": "ISC", "dependencies": { - "@lami/x-client-transaction-id": "npm:@jsr/lami__x-client-transaction-id@^0.1.7", "axios": "^1.8.4", "chalk": "^5.4.1", "commander": "^11.1.0", "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", - "node-html-parser": "^7.0.1" + "node-html-parser": "^7.0.1", + "x-client-transaction-id": "^0.1.7" }, "bin": { "rettiwt": "dist/cli.js" @@ -301,21 +301,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jsr/std__encoding": { - "version": "1.0.10", - "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", - "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==" - }, - "node_modules/@lami/x-client-transaction-id": { - "name": "@jsr/lami__x-client-transaction-id", - "version": "0.1.7", - "resolved": "https://npm.jsr.io/~/11/@jsr/lami__x-client-transaction-id/0.1.7.tgz", - "integrity": "sha512-B+KfQOuBjFVV8wu7ayhVhW6nKya0g/738BNokiePufu7EsL8ZRwVUXR7gUaGPLijhm49pGikMImQrMgOEBKx6w==", - "dependencies": { - "@jsr/std__encoding": "^1.0.10", - "linkedom": "^0.18.9" - } - }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", @@ -4454,6 +4439,15 @@ "node": ">=0.10.0" } }, + "node_modules/x-client-transaction-id": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.7.tgz", + "integrity": "sha512-fsXF2D3O4OVwtXtSTurjh34fc1HTe9EikFkPlT5i3eJGXe8lcXqZuNawGQ/6t+wsUuuFXuu9PkgM/bztALMlDw==", + "license": "MIT", + "dependencies": { + "linkedom": "^0.18.9" + } + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", diff --git a/package.json b/package.json index be7b19b5..a60faa63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.1", + "version": "6.0.2", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", @@ -45,12 +45,12 @@ "typescript": "^5.7.3" }, "dependencies": { - "@lami/x-client-transaction-id": "npm:@jsr/lami__x-client-transaction-id@^0.1.7", "axios": "^1.8.4", "chalk": "^5.4.1", "commander": "^11.1.0", "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", - "node-html-parser": "^7.0.1" + "node-html-parser": "^7.0.1", + "x-client-transaction-id": "^0.1.7" } } diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 78288c98..14dbd385 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,7 +1,6 @@ -/* eslint-disable import/no-unresolved */ -import { ClientTransaction, handleXMigration } from '@lami/x-client-transaction-id'; import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; +import { ClientTransaction, handleXMigration } from 'x-client-transaction-id'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; import { Requests } from '../../collections/Requests'; From 7ad8ddcfe47e2f5d1dd9a67e27630c4bac08188c Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Sun, 15 Jun 2025 19:53:19 +0000 Subject: [PATCH 020/119] Bumped version --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4db9bfdc..fbc9cbdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.1", + "version": "6.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.1", + "version": "6.0.2", "license": "ISC", "dependencies": { "axios": "^1.8.4", From 2006fb1b680a0b6e1b3231491fd1d12bde814e53 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sun, 29 Jun 2025 23:37:43 +0200 Subject: [PATCH 021/119] fix: type user service --- src/services/public/UserService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index d94a34b0..aef6a354 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -428,7 +428,7 @@ export class UserService extends FetcherService { * }); * ``` */ - public async highlights(id: string, count?: number, cursor?: string): Promise> { + public async highlights(id?: string, count?: number, cursor?: string): Promise> { const resource = ResourceType.USER_HIGHLIGHTS; // Fetching raw list of highlights @@ -719,7 +719,7 @@ export class UserService extends FetcherService { * }); * ``` */ - public async subscriptions(id: string, count?: number, cursor?: string): Promise> { + public async subscriptions(id?: string, count?: number, cursor?: string): Promise> { const resource = ResourceType.USER_SUBSCRIPTIONS; // Fetching raw list of subscriptions @@ -767,7 +767,7 @@ export class UserService extends FetcherService { * - If the target user has a pinned tweet, the returned timeline has one item extra and this is always the pinned tweet. * - If timeline is fetched without authenticating, then the most popular tweets of the target user are returned instead. */ - public async timeline(id: string, count?: number, cursor?: string): Promise> { + public async timeline(id?: string, count?: number, cursor?: string): Promise> { const resource = ResourceType.USER_TIMELINE; // Fetching raw list of tweets From 769469bff7f9bb76d6e9055ddb26ce18f910737d Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 00:53:52 +0200 Subject: [PATCH 022/119] refactor: update UserRequests to analytics v2 --- src/enums/raw/Analytics.ts | 9 ++++++--- src/requests/User.ts | 5 ++++- src/types/raw/base/Analytic.ts | 6 ++++++ src/types/raw/user/Analytics.ts | 19 +++++++++++++++---- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/enums/raw/Analytics.ts b/src/enums/raw/Analytics.ts index c49ce2e7..b41417f3 100644 --- a/src/enums/raw/Analytics.ts +++ b/src/enums/raw/Analytics.ts @@ -19,11 +19,14 @@ export enum RawAnalyticsMetric { IMPRESSIONS = 'Impressions', PROFILE_VISITS = 'ProfileVisits', FOLLOWS = 'Follows', - VIDEO_VIEWS = 'VideoViews', REPLIES = 'Replies', LIKES = 'Likes', RETWEETS = 'Retweets', - MEDIA_VIEWS = 'MediaViews', BOOKMARK = 'Bookmark', SHARE = 'Share', -} + URL_CLICKS = 'UrlClicks', + CREATE_TWEET = 'CreateTweet', + CREATE_QUOTE = 'CreateQuote', + CREATE_REPLY = 'CreateReply', + UNFOLLOWS = 'Unfollows', +} \ No newline at end of file diff --git a/src/requests/User.ts b/src/requests/User.ts index 6ed7d339..ba552c84 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -73,22 +73,25 @@ export class UserRequests { * @param toTime - The end time of the analytic data to be fetched. * @param granularity - The granularity of the analytic data to be fetched. * @param requestedMetrics - The metrics to be fetched. + * @param showVerifiedFollowers - Whether to show verified followers in the analytics. */ public static analytics( fromTime: Date, toTime: Date, granularity: RawAnalyticsGranularity, requestedMetrics: RawAnalyticsMetric[], + showVerifiedFollowers: boolean ): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/NlJ6RM-hgHxt-iu9cPQz7A/overviewDataUserQuery', + url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', params: { /* eslint-disable @typescript-eslint/naming-convention */ from_time: fromTime, to_time: toTime, granularity: granularity, requested_metrics: requestedMetrics, + show_verified_followers: showVerifiedFollowers, /* eslint-enable @typescript-eslint/naming-convention */ }, paramsSerializer: { encode: encodeURIComponent }, diff --git a/src/types/raw/base/Analytic.ts b/src/types/raw/base/Analytic.ts index 9a057e61..2837dfd8 100644 --- a/src/types/raw/base/Analytic.ts +++ b/src/types/raw/base/Analytic.ts @@ -8,9 +8,15 @@ export interface IAnalytics { __typename: string; organic_metrics_time_series: IAnalyticsMetric[]; + verified_follower_count: string; + relationship_counts: IAnalyticsRelationships; id: string; } +export interface IAnalyticsRelationships { + followers: number; +} + export interface IAnalyticsMetric { metric_value: IAnalyticsMetricValue[]; timestamp: IAnalyticsTimeStamp; diff --git a/src/types/raw/user/Analytics.ts b/src/types/raw/user/Analytics.ts index 35dc11e1..efae826b 100644 --- a/src/types/raw/user/Analytics.ts +++ b/src/types/raw/user/Analytics.ts @@ -10,20 +10,31 @@ export interface IUserAnalyticsResponse { } interface Data { - result: Result; + viewer_v2: ViewerV2; } -interface Result { - result: Result2; +interface ViewerV2 { + user_results: UserResults; +} + +interface UserResults { id: string; + result: Result; } -interface Result2 { + +interface Result { __typename: string; organic_metrics_time_series: Series[]; + verified_follower_count: string; + relationship_counts: Relationships; id: string; } +interface Relationships { + followers: number; +} + interface Series { metric_values: MetricValue[]; timestamp: Timestamp; From d835f127433081103ae70de62e0ab58b78464026 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 01:12:31 +0200 Subject: [PATCH 023/119] feat: add analytics to service --- src/collections/Requests.ts | 1 + src/enums/Resource.ts | 1 + src/requests/User.ts | 4 +-- src/services/public/UserService.ts | 52 ++++++++++++++++++++++++++++++ src/types/args/FetchArgs.ts | 42 ++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 6bedeb4d..f448ab9a 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -43,6 +43,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | TWEET_UNSCHEDULE: (args: IPostArgs) => TweetRequests.unschedule(args.id!), USER_AFFILIATES: (args: IFetchArgs) => UserRequests.affiliates(args.id!, args.count, args.cursor), + USER_ANALYTICS: (args: IFetchArgs) => UserRequests.analytics(args.fromTime!, args.toTime!, args.granularity!, args.metrics!, args.showVerifiedFollowers!), USER_BOOKMARKS: (args: IFetchArgs) => UserRequests.bookmarks(args.count, args.cursor), USER_DETAILS_BY_USERNAME: (args: IFetchArgs) => UserRequests.detailsByUsername(args.id!), USER_DETAILS_BY_ID: (args: IFetchArgs) => UserRequests.detailsById(args.id!), diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 4976e9c3..3da041ff 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -32,6 +32,7 @@ export enum ResourceType { // USER USER_AFFILIATES = 'USER_AFFILIATES', + USER_ANALYTICS = 'USER_ANALYTICS', USER_BOOKMARKS = 'USER_BOOKMARKS', USER_DETAILS_BY_USERNAME = 'USER_DETAILS_BY_USERNAME', USER_DETAILS_BY_ID = 'USER_DETAILS_BY_ID', diff --git a/src/requests/User.ts b/src/requests/User.ts index ba552c84..541777ff 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -87,8 +87,8 @@ export class UserRequests { url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', params: { /* eslint-disable @typescript-eslint/naming-convention */ - from_time: fromTime, - to_time: toTime, + from_time: fromTime.toISOString(), + to_time: toTime.toISOString(), granularity: granularity, requested_metrics: requestedMetrics, show_verified_followers: showVerifiedFollowers, diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index d94a34b0..3c4bfd0e 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -1,4 +1,5 @@ import { Extractors } from '../../collections/Extractors'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; import { ResourceType } from '../../enums/Resource'; import { CursoredData } from '../../models/data/CursoredData'; import { Notification } from '../../models/data/Notification'; @@ -6,6 +7,7 @@ import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates'; +import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics'; import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../../types/raw/user/DetailsBulk'; @@ -83,6 +85,56 @@ export class UserService extends FetcherService { return data; } + /** + * Get the analytics overview of the logged in user. + * + * @param fromTime - The start time of the analytics period. Defaults to 7 days ago. + * @param toTime - The end time of the analytics period. Defaults to now. + * @param granularity - The granularity of the analytics data. Defaults to daily. + * @param metrics - The metrics to include in the analytics data. Defaults to all available metrics available. + * @param showVerifiedFollowers - Whether to include verified follower count and relationship counts in the response. Defaults to true. + * + * @returns The raw analytics data of the user. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the analytics overview of the logged in user + * rettiwt.user.analytics().then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async analytics(fromTime?: Date, toTime?: Date, granularity?: RawAnalyticsGranularity, metrics?: RawAnalyticsMetric[], showVerifiedFollowers?: boolean): Promise { + const resource = ResourceType.USER_ANALYTICS; + + // Define default values if not provided + fromTime = fromTime || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago + toTime = toTime || new Date(); // Default to now + granularity = granularity || RawAnalyticsGranularity.DAILY; // Default to daily granularity + metrics = metrics || Object.values(RawAnalyticsMetric); // Default to all metrics + showVerifiedFollowers = showVerifiedFollowers || true; // Default to true + + // Fetching raw analytics + const response = await this.request(resource, { + fromTime: fromTime, + toTime: toTime, + granularity: granularity, + metrics: metrics, + showVerifiedFollowers: showVerifiedFollowers, + }); + + return response; + } + /** * Get the list of bookmarks of the logged in user. * diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index ddc78773..eea815a2 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -1,3 +1,4 @@ +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; import { TweetRepliesSortType } from '../../enums/Tweet'; /** @@ -65,6 +66,47 @@ export interface IFetchArgs { * - Only works for {@link EResourceType.TWEET_REPLIES}. */ sortBy?: TweetRepliesSortType; + + /** + * The date to start fetching data from. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + fromTime?: Date; + + /** + * The date to end fetching data at. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + toTime?: Date; + + /** + * The granularity of the data to fetch. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + granularity?: RawAnalyticsGranularity; + + /** + * The metrics to fetch. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + metrics?: RawAnalyticsMetric[]; + + /** + * Show the verified follower count and relationship counts in the response. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + showVerifiedFollowers?: boolean; + } /** From 8abf132ba3058c1f7adc6acd2c0bf61d51dec792 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 01:51:59 +0200 Subject: [PATCH 024/119] feat: data extractor for user analytics --- src/collections/Extractors.ts | 4 ++ src/collections/Requests.ts | 9 +++- src/enums/raw/Analytics.ts | 2 +- src/models/data/Analytics.ts | 86 ++++++++++++++++++++++++++++++ src/requests/User.ts | 2 +- src/services/public/UserService.ts | 27 ++++++---- src/types/args/FetchArgs.ts | 11 ++-- src/types/data/Analytics.ts | 58 ++++++++++++++++++++ src/types/raw/user/Analytics.ts | 31 +---------- 9 files changed, 183 insertions(+), 47 deletions(-) create mode 100644 src/models/data/Analytics.ts create mode 100644 src/types/data/Analytics.ts diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 67cc5c24..d35d3a15 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,4 +1,5 @@ import { BaseType } from '../enums/Data'; +import { Analytics } from '../models/data/Analytics'; import { CursoredData } from '../models/data/CursoredData'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; @@ -21,6 +22,7 @@ import { ITweetUnpostResponse } from '../types/raw/tweet/Unpost'; import { ITweetUnretweetResponse } from '../types/raw/tweet/Unretweet'; import { ITweetUnscheduleResponse } from '../types/raw/tweet/Unschedule'; import { IUserAffiliatesResponse } from '../types/raw/user/Affiliates'; +import { IUserAnalyticsResponse } from '../types/raw/user/Analytics'; import { IUserBookmarksResponse } from '../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../types/raw/user/DetailsBulk'; @@ -80,6 +82,8 @@ export const Extractors = { USER_AFFILIATES: (response: IUserAffiliatesResponse): CursoredData => new CursoredData(response, BaseType.USER), + USER_ANALYTICS: (response: IUserAnalyticsResponse): Analytics => + new Analytics(response.data.viewer_v2.user_results.result), USER_BOOKMARKS: (response: IUserBookmarksResponse): CursoredData => new CursoredData(response, BaseType.TWEET), USER_DETAILS_BY_USERNAME: (response: IUserDetailsResponse): User | undefined => User.single(response), diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index f448ab9a..95b85e87 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -43,7 +43,14 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | TWEET_UNSCHEDULE: (args: IPostArgs) => TweetRequests.unschedule(args.id!), USER_AFFILIATES: (args: IFetchArgs) => UserRequests.affiliates(args.id!, args.count, args.cursor), - USER_ANALYTICS: (args: IFetchArgs) => UserRequests.analytics(args.fromTime!, args.toTime!, args.granularity!, args.metrics!, args.showVerifiedFollowers!), + USER_ANALYTICS: (args: IFetchArgs) => + UserRequests.analytics( + args.fromTime!, + args.toTime!, + args.granularity!, + args.metrics!, + args.showVerifiedFollowers!, + ), USER_BOOKMARKS: (args: IFetchArgs) => UserRequests.bookmarks(args.count, args.cursor), USER_DETAILS_BY_USERNAME: (args: IFetchArgs) => UserRequests.detailsByUsername(args.id!), USER_DETAILS_BY_ID: (args: IFetchArgs) => UserRequests.detailsById(args.id!), diff --git a/src/enums/raw/Analytics.ts b/src/enums/raw/Analytics.ts index b41417f3..3c70d51c 100644 --- a/src/enums/raw/Analytics.ts +++ b/src/enums/raw/Analytics.ts @@ -29,4 +29,4 @@ export enum RawAnalyticsMetric { CREATE_QUOTE = 'CreateQuote', CREATE_REPLY = 'CreateReply', UNFOLLOWS = 'Unfollows', -} \ No newline at end of file +} diff --git a/src/models/data/Analytics.ts b/src/models/data/Analytics.ts new file mode 100644 index 00000000..4326e8f3 --- /dev/null +++ b/src/models/data/Analytics.ts @@ -0,0 +1,86 @@ +import { RawAnalyticsMetric } from '../../enums/raw/Analytics'; +import { IAnalytics as IRawAnalytics } from '../../types/raw/base/Analytic'; + +import type { IAnalytics } from '../../types/data/Analytics'; +import type { IAnalyticsMetric } from '../../types/raw/base/Analytic'; + +/** + * The details of the analytic result of the connected User. + * + * @public + */ +export class Analytics implements IAnalytics { + /** The raw analytic details. */ + private readonly _raw: IRawAnalytics; + + public bookmarks: number; + public createQuote: number; + public createReply: number; + public createTweets: number; + public createdAt: string; + public engagements: number; + public followers: number; + public follows: number; + public impressions: number; + public likes: number; + public organicMetricsTimeSeries: IAnalyticsMetric[]; + public profileVisits: number; + public replies: number; + public retweets: number; + public shares: number; + public unfollows: number; + public verifiedFollowers: number; + + public constructor(analytics: IRawAnalytics) { + this._raw = { ...analytics }; + this.createdAt = new Date().toISOString(); + this.followers = analytics.relationship_counts.followers; + this.verifiedFollowers = parseInt(analytics.verified_follower_count, 10); + this.impressions = this._reduceMetrics(RawAnalyticsMetric.IMPRESSIONS); + this.profileVisits = this._reduceMetrics(RawAnalyticsMetric.PROFILE_VISITS); + this.engagements = this._reduceMetrics(RawAnalyticsMetric.ENGAGEMENTS); + this.follows = this._reduceMetrics(RawAnalyticsMetric.FOLLOWS); + this.replies = this._reduceMetrics(RawAnalyticsMetric.REPLIES); + this.likes = this._reduceMetrics(RawAnalyticsMetric.LIKES); + this.retweets = this._reduceMetrics(RawAnalyticsMetric.RETWEETS); + this.bookmarks = this._reduceMetrics(RawAnalyticsMetric.BOOKMARK); + this.shares = this._reduceMetrics(RawAnalyticsMetric.SHARE); + this.createTweets = this._reduceMetrics(RawAnalyticsMetric.CREATE_TWEET); + this.createQuote = this._reduceMetrics(RawAnalyticsMetric.CREATE_QUOTE); + this.createReply = this._reduceMetrics(RawAnalyticsMetric.CREATE_REPLY); + this.unfollows = this._reduceMetrics(RawAnalyticsMetric.UNFOLLOWS); + this.organicMetricsTimeSeries = analytics.organic_metrics_time_series; + } + + private _reduceMetrics(metricType: RawAnalyticsMetric): number { + return this._raw.organic_metrics_time_series.reduce((acc, metric) => { + const metricValue = metric.metric_value.find((m) => m.metric_type === (metricType as string)); + return acc + (metricValue ? metricValue.metric_value : 0); + }, 0); + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IAnalytics { + return { + createdAt: this.createdAt, + followers: this.followers, + verifiedFollowers: this.verifiedFollowers, + impressions: this.impressions, + profileVisits: this.profileVisits, + engagements: this.engagements, + follows: this.follows, + replies: this.replies, + likes: this.likes, + retweets: this.retweets, + bookmarks: this.bookmarks, + shares: this.shares, + createTweets: this.createTweets, + createQuote: this.createQuote, + unfollows: this.unfollows, + createReply: this.createReply, + organicMetricsTimeSeries: this.organicMetricsTimeSeries + }; + } +} diff --git a/src/requests/User.ts b/src/requests/User.ts index 541777ff..a51c4ac2 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -80,7 +80,7 @@ export class UserRequests { toTime: Date, granularity: RawAnalyticsGranularity, requestedMetrics: RawAnalyticsMetric[], - showVerifiedFollowers: boolean + showVerifiedFollowers: boolean, ): AxiosRequestConfig { return { method: 'get', diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 3c4bfd0e..fbd53847 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -1,6 +1,7 @@ import { Extractors } from '../../collections/Extractors'; import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; import { ResourceType } from '../../enums/Resource'; +import { Analytics } from '../../models/data/Analytics'; import { CursoredData } from '../../models/data/CursoredData'; import { Notification } from '../../models/data/Notification'; import { Tweet } from '../../models/data/Tweet'; @@ -87,33 +88,39 @@ export class UserService extends FetcherService { /** * Get the analytics overview of the logged in user. - * + * * @param fromTime - The start time of the analytics period. Defaults to 7 days ago. * @param toTime - The end time of the analytics period. Defaults to now. * @param granularity - The granularity of the analytics data. Defaults to daily. * @param metrics - The metrics to include in the analytics data. Defaults to all available metrics available. * @param showVerifiedFollowers - Whether to include verified follower count and relationship counts in the response. Defaults to true. - * + * * @returns The raw analytics data of the user. - * + * * @example - * + * * ```ts * import { Rettiwt } from 'rettiwt-api'; - * + * * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); - * + * * // Fetching the analytics overview of the logged in user * rettiwt.user.analytics().then(res => { - * console.log(res); + * console.log(res); * }) * .catch(err => { * console.log(err); * }); * ``` */ - public async analytics(fromTime?: Date, toTime?: Date, granularity?: RawAnalyticsGranularity, metrics?: RawAnalyticsMetric[], showVerifiedFollowers?: boolean): Promise { + public async analytics( + fromTime?: Date, + toTime?: Date, + granularity?: RawAnalyticsGranularity, + metrics?: RawAnalyticsMetric[], + showVerifiedFollowers?: boolean, + ): Promise { const resource = ResourceType.USER_ANALYTICS; // Define default values if not provided @@ -132,7 +139,9 @@ export class UserService extends FetcherService { showVerifiedFollowers: showVerifiedFollowers, }); - return response; + const data = Extractors[resource](response); + + return data; } /** diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index eea815a2..2235c2fc 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -69,7 +69,7 @@ export interface IFetchArgs { /** * The date to start fetching data from. - * + * * @remarks * - Only works for {@link EResourceType.USER_ANALYTICS}. */ @@ -77,7 +77,7 @@ export interface IFetchArgs { /** * The date to end fetching data at. - * + * * @remarks * - Only works for {@link EResourceType.USER_ANALYTICS}. */ @@ -85,7 +85,7 @@ export interface IFetchArgs { /** * The granularity of the data to fetch. - * + * * @remarks * - Only works for {@link EResourceType.USER_ANALYTICS}. */ @@ -93,7 +93,7 @@ export interface IFetchArgs { /** * The metrics to fetch. - * + * * @remarks * - Only works for {@link EResourceType.USER_ANALYTICS}. */ @@ -101,12 +101,11 @@ export interface IFetchArgs { /** * Show the verified follower count and relationship counts in the response. - * + * * @remarks * - Only works for {@link EResourceType.USER_ANALYTICS}. */ showVerifiedFollowers?: boolean; - } /** diff --git a/src/types/data/Analytics.ts b/src/types/data/Analytics.ts new file mode 100644 index 00000000..619410ba --- /dev/null +++ b/src/types/data/Analytics.ts @@ -0,0 +1,58 @@ +import type { IAnalyticsMetric } from '../../types/raw/base/Analytic'; +/** + * The details of the analytic result of the connected User. + * + * @public + */ +export interface IAnalytics { + /** The creation date of user's account. */ + createdAt: string; + + /** Total followers number */ + followers: number; + + /** Total verified followers */ + verifiedFollowers: number; + + /** Total impressions on the given period */ + impressions: number; + + /** Total profile visits on the given period */ + profileVisits: number; + + /** Total Engagements on the given period */ + engagements: number; + + /** Total Follows on the given period */ + follows: number; + + /** Total Replies on the given period */ + replies: number; + + /** Total Likes on the given period */ + likes: number; + + /** Total Retweets on the given period */ + retweets: number; + + /** Total Bookmark on the given period */ + bookmarks: number; + + /** Total Shares on the given period */ + shares: number; + + /** Total CreateTweets on the given period */ + createTweets: number; + + /** Total CreateQuote on the given period */ + createQuote: number; + + /** Total Unfollows on the given period */ + unfollows: number; + + /** Total CreateReply on the given period */ + createReply: number; + + /** Organic metrics times series */ + organicMetricsTimeSeries: IAnalyticsMetric[]; +} diff --git a/src/types/raw/user/Analytics.ts b/src/types/raw/user/Analytics.ts index efae826b..dae1f66c 100644 --- a/src/types/raw/user/Analytics.ts +++ b/src/types/raw/user/Analytics.ts @@ -1,5 +1,5 @@ /* eslint-disable */ - +import type { IAnalytics } from '../base/Analytic'; /** * The raw data received when fetching the analytic overview of the user. * @@ -19,32 +19,5 @@ interface ViewerV2 { interface UserResults { id: string; - result: Result; -} - - -interface Result { - __typename: string; - organic_metrics_time_series: Series[]; - verified_follower_count: string; - relationship_counts: Relationships; - id: string; -} - -interface Relationships { - followers: number; -} - -interface Series { - metric_values: MetricValue[]; - timestamp: Timestamp; -} - -interface MetricValue { - metric_value: number; - metric_type: string; -} - -interface Timestamp { - iso8601_time: string; + result: IAnalytics; } From a6ebc100cc26d325dc197922f916e6c6eb3e48a0 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 15:39:48 +0200 Subject: [PATCH 025/119] fix: add analytics in fetch ressources group --- src/collections/Groups.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index bae4b568..692db5e0 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -27,6 +27,7 @@ export const FetchResourcesGroup = [ ResourceType.TWEET_RETWEETERS, ResourceType.TWEET_SEARCH, ResourceType.USER_AFFILIATES, + ResourceType.USER_ANALYTICS, ResourceType.USER_BOOKMARKS, ResourceType.USER_DETAILS_BY_USERNAME, ResourceType.USER_DETAILS_BY_ID, From feca459c31b6b9714e8929ef6eb671df21c7e218 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 15:45:44 +0200 Subject: [PATCH 026/119] fix: user analytics request --- src/requests/User.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requests/User.ts b/src/requests/User.ts index a51c4ac2..ec310dbd 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -87,8 +87,8 @@ export class UserRequests { url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', params: { /* eslint-disable @typescript-eslint/naming-convention */ - from_time: fromTime.toISOString(), - to_time: toTime.toISOString(), + from_time: fromTime, + to_time: toTime, granularity: granularity, requested_metrics: requestedMetrics, show_verified_followers: showVerifiedFollowers, From 33b641a59dba669e48af9890b1d06d442d463d99 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 16:42:24 +0200 Subject: [PATCH 027/119] fix: model args fetch --- src/models/args/FetchArgs.ts | 12 ++++++++++++ src/services/public/UserService.ts | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index e8af2209..27145556 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -1,6 +1,7 @@ import { TweetRepliesSortType } from '../../enums/Tweet'; import { IFetchArgs, ITweetFilter } from '../../types/args/FetchArgs'; +import type { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; /** * Options specifying the data that is to be fetched. * @@ -10,9 +11,15 @@ export class FetchArgs implements IFetchArgs { public count?: number; public cursor?: string; public filter?: TweetFilter; + public fromTime?: Date; + public granularity?: RawAnalyticsGranularity; public id?: string; public ids?: string[]; + public metrics?: RawAnalyticsMetric[]; + public showVerifiedFollowers?: boolean; public sortBy?: TweetRepliesSortType; + public toTime?: Date; + /** * @param args - Additional user-defined arguments for fetching the resource. @@ -24,6 +31,11 @@ export class FetchArgs implements IFetchArgs { this.cursor = args.cursor; this.filter = args.filter ? new TweetFilter(args.filter) : undefined; this.sortBy = args.sortBy; + this.fromTime = args.fromTime; + this.toTime = args.toTime; + this.granularity = args.granularity; + this.metrics = args.metrics; + this.showVerifiedFollowers = args.showVerifiedFollowers; } } diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index fbd53847..a0e93dfd 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -132,11 +132,11 @@ export class UserService extends FetcherService { // Fetching raw analytics const response = await this.request(resource, { - fromTime: fromTime, - toTime: toTime, - granularity: granularity, - metrics: metrics, - showVerifiedFollowers: showVerifiedFollowers, + fromTime, + toTime, + granularity, + metrics, + showVerifiedFollowers, }); const data = Extractors[resource](response); From f85a07210a81594c8a72b049de50e9da7f83901b Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 16:53:16 +0200 Subject: [PATCH 028/119] fix: user requests --- src/requests/User.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/requests/User.ts b/src/requests/User.ts index ec310dbd..71d115ce 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -82,17 +82,20 @@ export class UserRequests { requestedMetrics: RawAnalyticsMetric[], showVerifiedFollowers: boolean, ): AxiosRequestConfig { + console.log(`Fetching analytics from ${fromTime?.toString()} to ${toTime?.toString()} with granularity ${granularity} and metrics ${requestedMetrics.join(', ')}`); return { method: 'get', url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', params: { - /* eslint-disable @typescript-eslint/naming-convention */ - from_time: fromTime, - to_time: toTime, - granularity: granularity, - requested_metrics: requestedMetrics, - show_verified_followers: showVerifiedFollowers, - /* eslint-enable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + /* eslint-disable @typescript-eslint/naming-convention */ + from_time: fromTime, + to_time: toTime, + granularity: granularity, + requested_metrics: requestedMetrics, + show_verified_followers: showVerifiedFollowers, + /* eslint-enable @typescript-eslint/naming-convention */ + }), }, paramsSerializer: { encode: encodeURIComponent }, }; From 2ab4fc9730be0c6a9803920827ae04f7b5722857 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 17:03:11 +0200 Subject: [PATCH 029/119] fix: types response analytics --- src/models/data/Analytics.ts | 7 ++++--- src/types/raw/base/Analytic.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/models/data/Analytics.ts b/src/models/data/Analytics.ts index 4326e8f3..3e0ad5f6 100644 --- a/src/models/data/Analytics.ts +++ b/src/models/data/Analytics.ts @@ -33,6 +33,7 @@ export class Analytics implements IAnalytics { public constructor(analytics: IRawAnalytics) { this._raw = { ...analytics }; + this.organicMetricsTimeSeries = analytics.organic_metrics_time_series; this.createdAt = new Date().toISOString(); this.followers = analytics.relationship_counts.followers; this.verifiedFollowers = parseInt(analytics.verified_follower_count, 10); @@ -49,12 +50,12 @@ export class Analytics implements IAnalytics { this.createQuote = this._reduceMetrics(RawAnalyticsMetric.CREATE_QUOTE); this.createReply = this._reduceMetrics(RawAnalyticsMetric.CREATE_REPLY); this.unfollows = this._reduceMetrics(RawAnalyticsMetric.UNFOLLOWS); - this.organicMetricsTimeSeries = analytics.organic_metrics_time_series; + } private _reduceMetrics(metricType: RawAnalyticsMetric): number { - return this._raw.organic_metrics_time_series.reduce((acc, metric) => { - const metricValue = metric.metric_value.find((m) => m.metric_type === (metricType as string)); + return this.organicMetricsTimeSeries.reduce((acc, metric) => { + const metricValue = metric.metric_values.find((m) => m.metric_type === (metricType as string)); return acc + (metricValue ? metricValue.metric_value : 0); }, 0); } diff --git a/src/types/raw/base/Analytic.ts b/src/types/raw/base/Analytic.ts index 2837dfd8..9fda794a 100644 --- a/src/types/raw/base/Analytic.ts +++ b/src/types/raw/base/Analytic.ts @@ -18,7 +18,7 @@ export interface IAnalyticsRelationships { } export interface IAnalyticsMetric { - metric_value: IAnalyticsMetricValue[]; + metric_values: IAnalyticsMetricValue[]; timestamp: IAnalyticsTimeStamp; } From 88f54640ef049d0a0b4e3edfcd3464ca1c881ba1 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Mon, 30 Jun 2025 17:26:40 +0200 Subject: [PATCH 030/119] docs: update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ff741c50..2a70abac 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - Tweet Unretweet - Tweet Unschedule - User Affiliates + - User Analytics (**Only for Premium accounts**) - User Bookmarks - User Details - Single (by ID and Username) and Bulk (by ID only) - User Follow From f2a964d69b397a11210e2013141e53c2db4de39e Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Mon, 30 Jun 2025 16:07:24 +0000 Subject: [PATCH 031/119] Fixed unknown error while getting transaction token by upgrading x-client-transaction-id package --- package-lock.json | 14 +++++++------- package.json | 4 ++-- src/services/public/FetcherService.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbc9cbdc..75173fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.2", + "version": "6.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.2", + "version": "6.0.3", "license": "ISC", "dependencies": { "axios": "^1.8.4", @@ -15,7 +15,7 @@ "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", "node-html-parser": "^7.0.1", - "x-client-transaction-id": "^0.1.7" + "x-client-transaction-id-glacier": "^1.0.0" }, "bin": { "rettiwt": "dist/cli.js" @@ -4439,10 +4439,10 @@ "node": ">=0.10.0" } }, - "node_modules/x-client-transaction-id": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.7.tgz", - "integrity": "sha512-fsXF2D3O4OVwtXtSTurjh34fc1HTe9EikFkPlT5i3eJGXe8lcXqZuNawGQ/6t+wsUuuFXuu9PkgM/bztALMlDw==", + "node_modules/x-client-transaction-id-glacier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/x-client-transaction-id-glacier/-/x-client-transaction-id-glacier-1.0.0.tgz", + "integrity": "sha512-LmRJHgLTkksatezztTO+52SpGcwYLf90O2r2t4L9leAlhM5lOv/03c5izupwswmQAFA4Uk0str4qEV34KhBUAw==", "license": "MIT", "dependencies": { "linkedom": "^0.18.9" diff --git a/package.json b/package.json index a60faa63..9260a71f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.2", + "version": "6.0.3", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", @@ -51,6 +51,6 @@ "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", "node-html-parser": "^7.0.1", - "x-client-transaction-id": "^0.1.7" + "x-client-transaction-id-glacier": "^1.0.0" } } diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 14dbd385..52833d26 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,6 +1,6 @@ import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; -import { ClientTransaction, handleXMigration } from 'x-client-transaction-id'; +import { ClientTransaction, handleXMigration } from 'x-client-transaction-id-glacier'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; import { Requests } from '../../collections/Requests'; From 8c9675dffe34d388b98eb54cc413bf617a0897e8 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Mon, 30 Jun 2025 19:54:03 +0000 Subject: [PATCH 032/119] Fixed type for user.details --- package-lock.json | 4 +-- package.json | 2 +- src/services/public/UserService.ts | 57 ++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75173fb3..34a8b669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.3", + "version": "6.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.3", + "version": "6.0.4", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 9260a71f..89731af9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.3", + "version": "6.0.4", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index aef6a354..227e8334 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -125,16 +125,36 @@ export class UserService extends FetcherService { } /** - * Get the details of a user. + * Get the details of the logged in user. + * + * @returns The details of the user. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; * - * @param id - The username/ID/IDs of the target user/users. If no ID is provided, the logged-in user's ID is used. + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * - * @returns - * The details of the given user. + * // Fetching the details of the User + * rettiwt.user.details() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async details(): Promise; + + /** + * Get the details of a user. * - * If more than one ID is provided, returns a list. + * @param id - The ID/username of the target user. * - * If no user matches the given id, returns `undefined`. + * @returns The details of the user. * * @example * @@ -173,9 +193,18 @@ export class UserService extends FetcherService { * console.log(err); * }); * ``` - * * @example + */ + public async details(id: string): Promise; + + /** + * Get the details of multiple users in bulk. + * + * @param id - The list of IDs of the target users. + * + * @returns The details of the users. + * + * @example * - * #### Fetching the details of multiple users * ```ts * import { Rettiwt } from 'rettiwt-api'; * @@ -192,9 +221,9 @@ export class UserService extends FetcherService { * }); * ``` */ - public async details( - id: T, - ): Promise { + public async details(id: string[]): Promise; + + public async details(id?: string | string[]): Promise { let resource: ResourceType; // If details of multiple users required @@ -207,7 +236,7 @@ export class UserService extends FetcherService { // Deserializing response const data = Extractors[resource](response, id); - return data as T extends string | undefined ? User | undefined : User[]; + return data; } // If details of single user required else { @@ -222,7 +251,7 @@ export class UserService extends FetcherService { // If no ID is given, and not authenticated, skip if (!id && !this.config.userId) { - return undefined as T extends string | undefined ? User | undefined : User[]; + return undefined; } // Fetching raw details @@ -231,7 +260,7 @@ export class UserService extends FetcherService { // Deserializing response const data = Extractors[resource](response); - return data as T extends string | undefined ? User | undefined : User[]; + return data; } } From 6bb6d73914cdf598976a769ecfb368fb8e6637e0 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 1 Jul 2025 00:29:52 +0200 Subject: [PATCH 033/119] feat: add dm requests --- src/requests/DM.ts | 200 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/requests/DM.ts diff --git a/src/requests/DM.ts b/src/requests/DM.ts new file mode 100644 index 00000000..79e1d0d4 --- /dev/null +++ b/src/requests/DM.ts @@ -0,0 +1,200 @@ +import qs from 'querystring'; + +import { AxiosRequestConfig } from 'axios'; + +/** + * Common parameter sets for DM requests + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +const DM_BASE_PARAMS = { + /* eslint-disable @typescript-eslint/naming-convention */ + nsfw_filtering_enabled: false, + filter_low_quality: true, + include_quality: 'all', + dm_secret_conversations_enabled: false, + krs_registration_enabled: false, + cards_platform: 'Web-12', + include_cards: 1, + include_ext_alt_text: true, + include_ext_limited_action_results: true, + include_quote_count: true, + include_reply_count: 1, + tweet_mode: 'extended', + include_ext_views: true, + include_groups: true, + include_inbox_timelines: true, + include_ext_media_color: true, + supports_reactions: true, + supports_edit: true, + include_ext_edit_control: true, + include_ext_business_affiliations_label: true, + ext: 'mediaColor%2CaltText%2CbusinessAffiliationsLabel%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle' + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const DM_USER_INCLUDE_PARAMS = { + /* eslint-disable @typescript-eslint/naming-convention */ + include_profile_interstitial_type: 1, + include_blocking: 1, + include_blocked_by: 1, + include_followed_by: 1, + include_want_retweets: 1, + include_mute_edge: 1, + include_can_dm: 1, + include_can_media_tag: 1, + include_ext_is_blue_verified: 1, + include_ext_verified_type: 1, + include_ext_profile_image_shape: 1, + skip_status: 1 + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +/** + * Collection of requests related to direct messages. + * + * @public + */ +export class DMRequests { + /** + * Get a specific DM conversation + * @param conversationId - The conversation ID (e.g., "394028042-1712730991884689408") + * @param maxId - Maximum ID for pagination (optional) + */ + public static conversation(conversationId: string, maxId?: string): AxiosRequestConfig { + const context = maxId ? 'FETCH_DM_CONVERSATION_HISTORY' : 'FETCH_DM_CONVERSATION'; + + return { + method: 'get', + url: `https://x.com/i/api/1.1/dm/conversation/${conversationId}.json`, + params: { + ...DM_BASE_PARAMS, + ...DM_USER_INCLUDE_PARAMS, + /* eslint-disable @typescript-eslint/naming-convention */ + max_id: maxId, + context: context, + dm_users: false, + include_conversation_info: true + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Get the initial state of the DM inbox + */ + public static inboxInitial(): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/inbox_initial_state.json', + params: { + ...DM_BASE_PARAMS, + ...DM_USER_INCLUDE_PARAMS, + /* eslint-disable @typescript-eslint/naming-convention */ + dm_users: true, + include_ext_parody_commentary_fan_label: true, + ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle' + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Get inbox timeline (pagination of conversations) + * @param maxId - Maximum ID for pagination + */ + public static inboxTimeline(maxId?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/inbox_timeline/trusted.json', + params: { + ...DM_BASE_PARAMS, + ...DM_USER_INCLUDE_PARAMS, + /* eslint-disable @typescript-eslint/naming-convention */ + max_id: maxId, + dm_users: false + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Create a new DM or get DM creation interface + */ + public static new(): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/new2.json', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + include_ext_alt_text: true, + include_ext_limited_action_results: true, + include_reply_count: 1, + tweet_mode: 'extended', + include_ext_views: true, + include_groups: true, + include_inbox_timelines: true, + include_ext_media_color: true, + supports_reactions: true, + supports_edit: true + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Check DM permissions for specific recipients + * @param recipientIds - Array of recipient user IDs + */ + public static permissions(recipientIds: string[]): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/permissions.json', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + recipient_ids: recipientIds.join(','), + dm_users: true, + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Update the last seen event ID for a conversation + * @param lastSeenEventId - The ID of the last seen event + * @param trustedLastSeenEventId - The trusted last seen event ID (usually same as lastSeenEventId) + */ + public static updateLastSeenEventId(lastSeenEventId: string, trustedLastSeenEventId?: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/1.1/dm/update_last_seen_event_id.json', + data: qs.stringify({ + /* eslint-disable @typescript-eslint/naming-convention */ + last_seen_event_id: lastSeenEventId, + trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId + }), + }; + } + + /** + * Get user updates for DMs (polling for new messages) + * @param cursor - Cursor for pagination + * @param activeConversationId - ID of the currently active conversation + */ + public static userUpdates(cursor?: string, activeConversationId?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/user_updates.json', + params: { + ...DM_BASE_PARAMS, + /* eslint-disable @typescript-eslint/naming-convention */ + cursor: cursor, + active_conversation_id: activeConversationId, + dm_users: false + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } +} \ No newline at end of file From 4c30693db4a33dfe5c4719c860838d8a4223a07c Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 14:10:17 +0000 Subject: [PATCH 034/119] Revered x-client-transaction-id package to upstream version --- package-lock.json | 10 +++++----- package.json | 2 +- src/services/public/FetcherService.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34a8b669..86e384ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", "node-html-parser": "^7.0.1", - "x-client-transaction-id-glacier": "^1.0.0" + "x-client-transaction-id": "^0.1.8" }, "bin": { "rettiwt": "dist/cli.js" @@ -4439,10 +4439,10 @@ "node": ">=0.10.0" } }, - "node_modules/x-client-transaction-id-glacier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/x-client-transaction-id-glacier/-/x-client-transaction-id-glacier-1.0.0.tgz", - "integrity": "sha512-LmRJHgLTkksatezztTO+52SpGcwYLf90O2r2t4L9leAlhM5lOv/03c5izupwswmQAFA4Uk0str4qEV34KhBUAw==", + "node_modules/x-client-transaction-id": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.8.tgz", + "integrity": "sha512-0wYNIEKj124pKBHGWYb8Ux8CwtcUPAeUiqwM0KjW+NxGnuHQ3CnJF9k8rf1KekjbY8rVz37wXnKzrRPfaqnBMQ==", "license": "MIT", "dependencies": { "linkedom": "^0.18.9" diff --git a/package.json b/package.json index 89731af9..79ccf6c4 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,6 @@ "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", "node-html-parser": "^7.0.1", - "x-client-transaction-id-glacier": "^1.0.0" + "x-client-transaction-id": "^0.1.8" } } diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 52833d26..14dbd385 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,6 +1,6 @@ import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; -import { ClientTransaction, handleXMigration } from 'x-client-transaction-id-glacier'; +import { ClientTransaction, handleXMigration } from 'x-client-transaction-id'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; import { Requests } from '../../collections/Requests'; From b8d16399dd03986d5a62330dc6ee03f8b39ec84c Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 1 Jul 2025 16:26:23 +0200 Subject: [PATCH 035/119] feat: getter raw, linting, format --- src/models/args/FetchArgs.ts | 1 - src/models/data/Analytics.ts | 56 +++++++++++++++++++++--------------- src/requests/User.ts | 4 ++- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index 27145556..5350b982 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -20,7 +20,6 @@ export class FetchArgs implements IFetchArgs { public sortBy?: TweetRepliesSortType; public toTime?: Date; - /** * @param args - Additional user-defined arguments for fetching the resource. */ diff --git a/src/models/data/Analytics.ts b/src/models/data/Analytics.ts index 3e0ad5f6..4e420aae 100644 --- a/src/models/data/Analytics.ts +++ b/src/models/data/Analytics.ts @@ -50,38 +50,48 @@ export class Analytics implements IAnalytics { this.createQuote = this._reduceMetrics(RawAnalyticsMetric.CREATE_QUOTE); this.createReply = this._reduceMetrics(RawAnalyticsMetric.CREATE_REPLY); this.unfollows = this._reduceMetrics(RawAnalyticsMetric.UNFOLLOWS); - } - private _reduceMetrics(metricType: RawAnalyticsMetric): number { + /** The raw analytic details. */ + public get raw(): IRawAnalytics { + return { ...this._raw }; + } + + /** + * Reduces the organic metrics time series to a total value for a specific metric type. + * + * @param metricType The type of metric to reduce. + * @returns the total value of the specified metric type across all time series. + */ + private _reduceMetrics(metricType: RawAnalyticsMetric): number { return this.organicMetricsTimeSeries.reduce((acc, metric) => { const metricValue = metric.metric_values.find((m) => m.metric_type === (metricType as string)); return acc + (metricValue ? metricValue.metric_value : 0); }, 0); } - /** + /** * @returns A serializable JSON representation of `this` object. */ public toJSON(): IAnalytics { - return { - createdAt: this.createdAt, - followers: this.followers, - verifiedFollowers: this.verifiedFollowers, - impressions: this.impressions, - profileVisits: this.profileVisits, - engagements: this.engagements, - follows: this.follows, - replies: this.replies, - likes: this.likes, - retweets: this.retweets, - bookmarks: this.bookmarks, - shares: this.shares, - createTweets: this.createTweets, - createQuote: this.createQuote, - unfollows: this.unfollows, - createReply: this.createReply, - organicMetricsTimeSeries: this.organicMetricsTimeSeries - }; - } + return { + createdAt: this.createdAt, + followers: this.followers, + verifiedFollowers: this.verifiedFollowers, + impressions: this.impressions, + profileVisits: this.profileVisits, + engagements: this.engagements, + follows: this.follows, + replies: this.replies, + likes: this.likes, + retweets: this.retweets, + bookmarks: this.bookmarks, + shares: this.shares, + createTweets: this.createTweets, + createQuote: this.createQuote, + unfollows: this.unfollows, + createReply: this.createReply, + organicMetricsTimeSeries: this.organicMetricsTimeSeries, + }; + } } diff --git a/src/requests/User.ts b/src/requests/User.ts index 71d115ce..10cbc38b 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -82,7 +82,9 @@ export class UserRequests { requestedMetrics: RawAnalyticsMetric[], showVerifiedFollowers: boolean, ): AxiosRequestConfig { - console.log(`Fetching analytics from ${fromTime?.toString()} to ${toTime?.toString()} with granularity ${granularity} and metrics ${requestedMetrics.join(', ')}`); + console.log( + `Fetching analytics from ${fromTime?.toString()} to ${toTime?.toString()} with granularity ${granularity} and metrics ${requestedMetrics.join(', ')}`, + ); return { method: 'get', url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', From f78addc2e13b473c72d3275d053c7d45f46a4fd5 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 14:30:43 +0000 Subject: [PATCH 036/119] Added ability to bookmark a tweet --- README.md | 6 ++-- src/collections/Extractors.ts | 3 ++ src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/Tweet.ts | 14 ++++++++++ src/enums/Resource.ts | 1 + src/requests/Tweet.ts | 15 ++++++++++ src/services/public/FetcherService.ts | 4 +-- src/services/public/TweetService.ts | 40 +++++++++++++++++++++++++++ src/types/args/FetchArgs.ts | 24 ++++++++-------- src/types/args/PostArgs.ts | 23 +++++++-------- src/types/raw/tweet/Bookmark.ts | 14 ++++++++++ 12 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 src/types/raw/tweet/Bookmark.ts diff --git a/README.md b/README.md index ff741c50..08b44fd6 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ rettiwt.user.details('') However, if further control over the raw response is required, Rettiwt-API provides the [`FetcherService`](https://rishikant181.github.io/Rettiwt-API/classes/FetcherService.html) class which provides direct access to the raw response, but keep in mind, this delegates the task of parsing and filtering the results to the consumer of the library. The following example demonstrates using the `FetcherService` class: ```ts -import { RettiwtConfig, FetcherService, EResourceType, IUserDetailsResponse } from 'rettiwt-api'; +import { RettiwtConfig, FetcherService, ResourceType, IUserDetailsResponse } from 'rettiwt-api'; // Creating the configuration for Rettiwt const config = new RettiwtConfig({ apiKey: '' }); @@ -409,7 +409,7 @@ const fetcher = new FetcherService(config); // Fetching the details of the given user fetcher - .request(EResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) + .request(ResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) .then((res) => { console.log(res); }) @@ -418,7 +418,7 @@ fetcher }); ``` -As demonstrated by the example, the raw data can be accessed by using the `request` method of the `FetcherService` class, which takes two parameters. The first parameter is the name of the requested resource, while the second is an object specifying the associated arguments required for the given resource. The complete list of resource type can be checked [here](https://rishikant181.github.io/Rettiwt-API/enums/AuthService.html#EResourceType). As for the resource specific argurments, they are the same as that of the methods of `Rettiwt` class' methods for the respective resources, but structured as an object. Notice how the `FetcherService` class takes the same arguments as the `Rettiwt` class, and the arguments have the same effects as they have in case of `Rettiwt` class. +As demonstrated by the example, the raw data can be accessed by using the `request` method of the `FetcherService` class, which takes two parameters. The first parameter is the name of the requested resource, while the second is an object specifying the associated arguments required for the given resource. The complete list of resource type can be checked [here](https://rishikant181.github.io/Rettiwt-API/enums/AuthService.html#ResourceType). As for the resource specific argurments, they are the same as that of the methods of `Rettiwt` class' methods for the respective resources, but structured as an object. Notice how the `FetcherService` class takes the same arguments as the `Rettiwt` class, and the arguments have the same effects as they have in case of `Rettiwt` class. #### Notes: diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 67cc5c24..34f6a69f 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -6,6 +6,7 @@ import { User } from '../models/data/User'; import { IListMembersResponse } from '../types/raw/list/Members'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; +import { ITweetBookmarkResponse } from '../types/raw/tweet/Bookmark'; import { ITweetDetailsResponse } from '../types/raw/tweet/Details'; import { ITweetDetailsBulkResponse } from '../types/raw/tweet/DetailsBulk'; import { ITweetLikeResponse } from '../types/raw/tweet/Like'; @@ -56,6 +57,8 @@ export const Extractors = { MEDIA_UPLOAD_INITIALIZE: (response: IMediaInitializeUploadResponse): string => response.media_id_string ?? undefined, + TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => + response?.data?.tweet_bookmark_put === 'Done' ? true : false, TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_ALT: (response: ITweetRepliesResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_BULK: (response: ITweetDetailsBulkResponse, ids: string[]): Tweet[] => Tweet.multiple(response, ids), diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index bae4b568..86948cfd 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -53,6 +53,7 @@ export const PostResourcesGroup = [ ResourceType.MEDIA_UPLOAD_APPEND, ResourceType.MEDIA_UPLOAD_FINALIZE, ResourceType.MEDIA_UPLOAD_INITIALIZE, + ResourceType.TWEET_BOOKMARK, ResourceType.TWEET_LIKE, ResourceType.TWEET_POST, ResourceType.TWEET_RETWEET, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 6bedeb4d..0eae2f0a 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -25,6 +25,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | MEDIA_UPLOAD_FINALIZE: (args: IPostArgs) => MediaRequests.finalizeUpload(args.upload!.id!), MEDIA_UPLOAD_INITIALIZE: (args: IPostArgs) => MediaRequests.initializeUpload(args.upload!.size!), + TWEET_BOOKMARK: (args: IPostArgs) => TweetRequests.bookmark(args.id!), TWEET_DETAILS: (args: IFetchArgs) => TweetRequests.details(args.id!), TWEET_DETAILS_ALT: (args: IFetchArgs) => TweetRequests.replies(args.id!), TWEET_DETAILS_BULK: (args: IFetchArgs) => TweetRequests.bulkDetails(args.ids!), diff --git a/src/commands/Tweet.ts b/src/commands/Tweet.ts index ca2b9d8c..eca10e93 100644 --- a/src/commands/Tweet.ts +++ b/src/commands/Tweet.ts @@ -16,6 +16,20 @@ function createTweetCommand(rettiwt: Rettiwt): Command { // Creating the 'tweet' command const tweet = createCommand('tweet').description('Access resources releated to tweets'); + // Bookmark + tweet + .command('bookmark') + .description('Bookmark a tweet') + .argument('', 'The tweet to bookmark') + .action(async (id: string) => { + try { + const result = await rettiwt.tweet.bookmark(id); + output(result); + } catch (error) { + output(error); + } + }); + // Details tweet .command('details') diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 4976e9c3..20b144b2 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -14,6 +14,7 @@ export enum ResourceType { MEDIA_UPLOAD_INITIALIZE = 'MEDIA_UPLOAD_INITIALIZE', // TWEET + TWEET_BOOKMARK = 'TWEET_BOOKMARK', TWEET_DETAILS = 'TWEET_DETAILS', TWEET_DETAILS_ALT = 'TWEET_DETAILS_ALT', TWEET_DETAILS_BULK = 'TWEET_DETAILS_BULK', diff --git a/src/requests/Tweet.ts b/src/requests/Tweet.ts index 1280bf87..6af690cc 100644 --- a/src/requests/Tweet.ts +++ b/src/requests/Tweet.ts @@ -13,6 +13,21 @@ import { INewTweet } from '../types/args/PostArgs'; * @public */ export class TweetRequests { + /** + * @param id - The ID of the tweet to bookmark + */ + public static bookmark(id: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/aoDbu3RHznuiSkQ9aNM67Q/CreateBookmark', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ tweet_id: id }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param ids - The IDs of the tweets whose details are to be fetched. */ diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 14dbd385..02890f8c 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -180,13 +180,13 @@ export class FetcherService { * * #### Fetching the raw details of a single user, using their username * ```ts - * import { FetcherService, EResourceType } from 'rettiwt-api'; + * import { FetcherService, ResourceType } from 'rettiwt-api'; * * // Creating a new FetcherService instance using the given 'API_KEY' * const fetcher = new FetcherService({ apiKey: API_KEY }); * * // Fetching the details of the User with username 'user1' - * fetcher.request(EResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) + * fetcher.request(ResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) * .then(res => { * console.log(res); * }) diff --git a/src/services/public/TweetService.ts b/src/services/public/TweetService.ts index 1327429a..c42bfcee 100644 --- a/src/services/public/TweetService.ts +++ b/src/services/public/TweetService.ts @@ -12,6 +12,7 @@ import { ITweetFilter } from '../../types/args/FetchArgs'; import { INewTweet } from '../../types/args/PostArgs'; import { IMediaInitializeUploadResponse } from '../../types/raw/media/InitalizeUpload'; +import { ITweetBookmarkResponse } from '../../types/raw/tweet/Bookmark'; import { ITweetDetailsResponse } from '../../types/raw/tweet/Details'; import { ITweetDetailsBulkResponse } from '../../types/raw/tweet/DetailsBulk'; import { ITweetLikeResponse } from '../../types/raw/tweet/Like'; @@ -44,6 +45,45 @@ export class TweetService extends FetcherService { super(config); } + /** + * Bookmark a tweet. + * + * @param id - The ID of the tweet to be bookmarked. + * + * @returns Whether bookmarking was successful or not. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Bookmarking the Tweet with id '1234567890' + * rettiwt.tweet.bookmark('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmark(id: string): Promise { + const resource = ResourceType.TWEET_BOOKMARK; + + // Favoriting the tweet + const response = await this.request(resource, { + id: id, + }); + + // Deserializing response + const data = Extractors[resource](response) ?? false; + + return data; + } + /** * Get the details of one or more tweets. * diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index ddc78773..793ea673 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -11,16 +11,16 @@ export interface IFetchArgs { * * @remarks * - Works only for cursored resources. - * - Does not work for {@link EResourceType.TWEET_REPLIES}. + * - Does not work for {@link ResourceType.TWEET_REPLIES}. * - Must be \<= 20 for: - * - {@link EResourceType.USER_TIMELINE} - * - {@link EResourceType.USER_TIMELINE} - * - {@link EResourceType.USER_TIMELINE_AND_REPLIES} + * - {@link ResourceType.USER_TIMELINE} + * - {@link ResourceType.USER_TIMELINE} + * - {@link ResourceType.USER_TIMELINE_AND_REPLIES} * - Must be \<= 100 for all other cursored resources. - * - Due a bug on Twitter's end, count does not work for {@link EResourceType.USER_FOLLOWERS} and {@link EResourceType.USER_FOLLOWING}. + * - Due a bug on Twitter's end, count does not work for {@link ResourceType.USER_FOLLOWERS} and {@link ResourceType.USER_FOLLOWING}. * - Has not effect for: - * - {@link EResourceType.USER_FEED_FOLLOWED} - * - {@link EResourceType.USER_FEED_RECOMMENDED} + * - {@link ResourceType.USER_FEED_FOLLOWED} + * - {@link ResourceType.USER_FEED_RECOMMENDED} */ count?: number; @@ -37,7 +37,7 @@ export interface IFetchArgs { * The filter for searching tweets. * * @remarks - * Required when searching for tweets using {@link EResourceType.TWEET_SEARCH}. + * Required when searching for tweets using {@link ResourceType.TWEET_SEARCH}. */ filter?: ITweetFilter; @@ -45,8 +45,8 @@ export interface IFetchArgs { * The id of the target resource. * * @remarks - * - Required for all resources except {@link EResourceType.TWEET_SEARCH} and {@link EResourceType.USER_TIMELINE_RECOMMENDED}. - * - For {@link EResourceType.USER_DETAILS_BY_USERNAME}, can be alphanumeric, while for others, is strictly numeric. + * - Required for all resources except {@link ResourceType.TWEET_SEARCH} and {@link ResourceType.USER_TIMELINE_RECOMMENDED}. + * - For {@link ResourceType.USER_DETAILS_BY_USERNAME}, can be alphanumeric, while for others, is strictly numeric. */ id?: string; @@ -54,7 +54,7 @@ export interface IFetchArgs { * The IDs of the target resources. * * @remarks - * - Required only for {@link EResourceType.TWEET_DETAILS_BULK} and {@link EResourceType.USER_DETAILS_BY_IDS_BULK}. + * - Required only for {@link ResourceType.TWEET_DETAILS_BULK} and {@link ResourceType.USER_DETAILS_BY_IDS_BULK}. */ ids?: string[]; @@ -62,7 +62,7 @@ export interface IFetchArgs { * The sorting to use for tweet results. * * @remarks - * - Only works for {@link EResourceType.TWEET_REPLIES}. + * - Only works for {@link ResourceType.TWEET_REPLIES}. */ sortBy?: TweetRepliesSortType; } diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index 7efde397..647650c3 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -9,13 +9,14 @@ export interface IPostArgs { * * @remarks * Required only when posting using the following resources: - * - {@link EResourceType.TWEET_LIKE} - * - {@link EResourceType.TWEET_RETWEET} - * - {@link EResourceType.TWEET_UNLIKE} - * - {@link EResourceType.TWEET_UNPOST} - * - {@link EResourceType.TWEET_UNRETWEET} - * - {@link EResourceType.USER_FOLLOW} - * - {@link EResourceType.USER_UNFOLLOW} + * - {@link ResourceType.TWEET_BOOKMARK} + * - {@link ResourceType.TWEET_LIKE} + * - {@link ResourceType.TWEET_RETWEET} + * - {@link ResourceType.TWEET_UNLIKE} + * - {@link ResourceType.TWEET_UNPOST} + * - {@link ResourceType.TWEET_UNRETWEET} + * - {@link ResourceType.USER_FOLLOW} + * - {@link ResourceType.USER_UNFOLLOW} */ id?: string; @@ -23,7 +24,7 @@ export interface IPostArgs { * The tweet that is to be posted. * * @remarks - * Required only when posting a tweet using {@link EResourceType.TWEET_POST} + * Required only when posting a tweet using {@link ResourceType.TWEET_POST} */ tweet?: INewTweet; @@ -32,9 +33,9 @@ export interface IPostArgs { * * @remarks * Required only when uploading a media using the following resources: - * - {@link EResourceType.MEDIA_UPLOAD_APPEND} - * - {@link EResourceType.MEDIA_UPLOAD_FINALIZE} - * - {@link EResourceType.MEDIA_UPLOAD_INITIALIZE} + * - {@link ResourceType.MEDIA_UPLOAD_APPEND} + * - {@link ResourceType.MEDIA_UPLOAD_FINALIZE} + * - {@link ResourceType.MEDIA_UPLOAD_INITIALIZE} */ upload?: IUploadArgs; } diff --git a/src/types/raw/tweet/Bookmark.ts b/src/types/raw/tweet/Bookmark.ts new file mode 100644 index 00000000..caa55d5c --- /dev/null +++ b/src/types/raw/tweet/Bookmark.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * The raw data received when bookmarking a given tweet. + * + * @public + */ +export interface ITweetBookmarkResponse { + data: Data; +} + +interface Data { + tweet_bookmark_put: string; +} From 73bfaa3d7a2f30558e6d8b387e5e8462108d97ce Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 14:39:59 +0000 Subject: [PATCH 037/119] Added ability to unbookmark a tweet --- src/collections/Extractors.ts | 5 ++-- src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/Tweet.ts | 14 +++++++++++ src/enums/Resource.ts | 1 + src/requests/Tweet.ts | 17 +++++++++++++ src/services/public/TweetService.ts | 38 +++++++++++++++++++++++++++++ src/types/args/PostArgs.ts | 1 + src/types/raw/tweet/Unbookmark.ts | 14 +++++++++++ 9 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/types/raw/tweet/Unbookmark.ts diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 34f6a69f..82190dc2 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -17,6 +17,7 @@ import { ITweetRetweetResponse } from '../types/raw/tweet/Retweet'; import { ITweetRetweetersResponse } from '../types/raw/tweet/Retweeters'; import { ITweetScheduleResponse } from '../types/raw/tweet/Schedule'; import { ITweetSearchResponse } from '../types/raw/tweet/Search'; +import { ITweetUnbookmarkResponse } from '../types/raw/tweet/Unbookmark'; import { ITweetUnlikeResponse } from '../types/raw/tweet/Unlike'; import { ITweetUnpostResponse } from '../types/raw/tweet/Unpost'; import { ITweetUnretweetResponse } from '../types/raw/tweet/Unretweet'; @@ -57,8 +58,7 @@ export const Extractors = { MEDIA_UPLOAD_INITIALIZE: (response: IMediaInitializeUploadResponse): string => response.media_id_string ?? undefined, - TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => - response?.data?.tweet_bookmark_put === 'Done' ? true : false, + TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => response?.data?.tweet_bookmark_put === 'Done', TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_ALT: (response: ITweetRepliesResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_BULK: (response: ITweetDetailsBulkResponse, ids: string[]): Tweet[] => Tweet.multiple(response, ids), @@ -75,6 +75,7 @@ export const Extractors = { TWEET_SCHEDULE: (response: ITweetScheduleResponse): string => response?.data?.tweet?.rest_id ?? undefined, TWEET_SEARCH: (response: ITweetSearchResponse): CursoredData => new CursoredData(response, BaseType.TWEET), + TWEET_UNBOOKMARK: (response: ITweetUnbookmarkResponse): boolean => response?.data?.tweet_bookmark_delete === 'Done', TWEET_UNLIKE: (response: ITweetUnlikeResponse): boolean => (response?.data?.unfavorite_tweet ? true : false), TWEET_UNPOST: (response: ITweetUnpostResponse): boolean => (response?.data?.delete_tweet ? true : false), TWEET_UNRETWEET: (response: ITweetUnretweetResponse): boolean => diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 86948cfd..f3344604 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -58,6 +58,7 @@ export const PostResourcesGroup = [ ResourceType.TWEET_POST, ResourceType.TWEET_RETWEET, ResourceType.TWEET_SCHEDULE, + ResourceType.TWEET_UNBOOKMARK, ResourceType.TWEET_UNLIKE, ResourceType.TWEET_UNPOST, ResourceType.TWEET_UNRETWEET, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 0eae2f0a..d2cd9cf8 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -38,6 +38,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | TWEET_RETWEETERS: (args: IFetchArgs) => TweetRequests.retweeters(args.id!, args.count, args.cursor), TWEET_SCHEDULE: (args: IPostArgs) => TweetRequests.schedule(args.tweet!), TWEET_SEARCH: (args: IFetchArgs) => TweetRequests.search(args.filter!, args.count, args.cursor), + TWEET_UNBOOKMARK: (args: IPostArgs) => TweetRequests.unbookmark(args.id!), TWEET_UNLIKE: (args: IPostArgs) => TweetRequests.unlike(args.id!), TWEET_UNPOST: (args: IPostArgs) => TweetRequests.unpost(args.id!), TWEET_UNRETWEET: (args: IPostArgs) => TweetRequests.unretweet(args.id!), diff --git a/src/commands/Tweet.ts b/src/commands/Tweet.ts index eca10e93..731331cf 100644 --- a/src/commands/Tweet.ts +++ b/src/commands/Tweet.ts @@ -257,6 +257,20 @@ function createTweetCommand(rettiwt: Rettiwt): Command { } }); + // Unbookmark + tweet + .command('unbookmark') + .description('Unbookmark a tweet') + .argument('', 'The id of the tweet') + .action(async (id: string) => { + try { + const result = await rettiwt.tweet.unbookmark(id); + output(result); + } catch (error) { + output(error); + } + }); + // Unlike tweet .command('unlike') diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 20b144b2..98872f99 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -26,6 +26,7 @@ export enum ResourceType { TWEET_RETWEETERS = 'TWEET_RETWEETERS', TWEET_SCHEDULE = 'TWEET_SCHEDULE', TWEET_SEARCH = 'TWEET_SEARCH', + TWEET_UNBOOKMARK = 'TWEET_UNBOOKMARK', TWEET_UNLIKE = 'TWEET_UNLIKE', TWEET_UNPOST = 'TWEET_UNPOST', TWEET_UNRETWEET = 'TWEET_UNRETWEET', diff --git a/src/requests/Tweet.ts b/src/requests/Tweet.ts index 6af690cc..c9d1ae92 100644 --- a/src/requests/Tweet.ts +++ b/src/requests/Tweet.ts @@ -516,6 +516,23 @@ export class TweetRequests { }; } + /** + * @param id - The id of the tweet to be unbookmarked. + */ + public static unbookmark(id: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/Wlmlj2-xzyS1GN3a6cj-mQ/DeleteBookmark', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: { + tweet_id: id, + }, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the tweet to be unliked. */ diff --git a/src/services/public/TweetService.ts b/src/services/public/TweetService.ts index c42bfcee..87a8f42c 100644 --- a/src/services/public/TweetService.ts +++ b/src/services/public/TweetService.ts @@ -23,6 +23,7 @@ import { ITweetRetweetResponse } from '../../types/raw/tweet/Retweet'; import { ITweetRetweetersResponse } from '../../types/raw/tweet/Retweeters'; import { ITweetScheduleResponse } from '../../types/raw/tweet/Schedule'; import { ITweetSearchResponse } from '../../types/raw/tweet/Search'; +import { ITweetUnbookmarkResponse } from '../../types/raw/tweet/Unbookmark'; import { ITweetUnlikeResponse } from '../../types/raw/tweet/Unlike'; import { ITweetUnpostResponse } from '../../types/raw/tweet/Unpost'; import { ITweetUnretweetResponse } from '../../types/raw/tweet/Unretweet'; @@ -645,6 +646,43 @@ export class TweetService extends FetcherService { } } + /** + * Unbookmark a tweet. + * + * @param id - The ID of the target tweet. + * + * @returns Whether unbookmarking was successful or not. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Unbookmarking the tweet with id '1234567890' + * rettiwt.tweet.unbookmark('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async unbookmark(id: string): Promise { + const resource = ResourceType.TWEET_UNBOOKMARK; + + // Unliking the tweet + const response = await this.request(resource, { id: id }); + + // Deserializing the response + const data = Extractors[resource](response) ?? false; + + return data; + } + /** * Unlike a tweet. * diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index 647650c3..4930f5f4 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -12,6 +12,7 @@ export interface IPostArgs { * - {@link ResourceType.TWEET_BOOKMARK} * - {@link ResourceType.TWEET_LIKE} * - {@link ResourceType.TWEET_RETWEET} + * - {@link ResourceType.TWEET_UNBOOKMARK} * - {@link ResourceType.TWEET_UNLIKE} * - {@link ResourceType.TWEET_UNPOST} * - {@link ResourceType.TWEET_UNRETWEET} diff --git a/src/types/raw/tweet/Unbookmark.ts b/src/types/raw/tweet/Unbookmark.ts new file mode 100644 index 00000000..05ac54c6 --- /dev/null +++ b/src/types/raw/tweet/Unbookmark.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * The raw data received when unbookmarking a given tweet. + * + * @public + */ +export interface ITweetUnbookmarkResponse { + data: Data; +} + +export interface Data { + tweet_bookmark_delete: string; +} From 201841900dad2103050e5cb0bf15b5a36a9239ca Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 14:41:39 +0000 Subject: [PATCH 038/119] Updated README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 08b44fd6..234451cf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - List Members - List Tweets - Tweet Details - Single and Bulk + - Tweet Bookmark - Tweet Like - Tweet Likers - Tweet Media Upload @@ -42,6 +43,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - Tweet Schedule - Tweet Search - Tweet Stream + - Tweet Unbookmark - Tweet Unlike - Tweet Unpost - Tweet Unretweet @@ -441,6 +443,7 @@ So far, the following operations are supported: ### Tweets +- [Bookmarking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#bookmark) - [Getting the details of a tweet/multiple tweets](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#details) - [Liking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#like) - [Getting the list of users who liked your tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#likers) @@ -451,6 +454,7 @@ So far, the following operations are supported: - [Scheduling a new tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#schedule) - [Searching for the list of tweets that match a given filter](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#search) - [Streaming filtered tweets in pseudo-realtime](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#stream) +- [Unbookmarking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unbookmark) - [Unliking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unlike) - [Unposting a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unpost) - [Unretweeting a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unretweet) From 6be4a428c5ee0c744d9bed70844e7df2180c4fa6 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 14:54:10 +0000 Subject: [PATCH 039/119] Updated README.md --- README.md | 3 ++- src/models/data/Analytics.ts | 2 +- src/services/public/UserService.ts | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e5d21877..d256590f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - Tweet Unretweet - Tweet Unschedule - User Affiliates - - User Analytics (**Only for Premium accounts**) + - User Analytics (Only for Premium accounts) - User Bookmarks - User Details - Single (by ID and Username) and Bulk (by ID only) - User Follow @@ -465,6 +465,7 @@ So far, the following operations are supported: ### Users - [Getting the list of users affiliated with the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#affiliates) +- [Getting the analytics of the logged-in user (premium accounts only)](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#analytics) - [Getting the list of tweets bookmarked by the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarks) - [Getting the details of a user/multiple users](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#details) - [Following a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#follow) diff --git a/src/models/data/Analytics.ts b/src/models/data/Analytics.ts index 4e420aae..1a3c5714 100644 --- a/src/models/data/Analytics.ts +++ b/src/models/data/Analytics.ts @@ -60,7 +60,7 @@ export class Analytics implements IAnalytics { /** * Reduces the organic metrics time series to a total value for a specific metric type. * - * @param metricType The type of metric to reduce. + * @param metricType - metricType The type of metric to reduce. * @returns the total value of the specified metric type across all time series. */ private _reduceMetrics(metricType: RawAnalyticsMetric): number { diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 84d6689d..e71075a0 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -124,11 +124,11 @@ export class UserService extends FetcherService { const resource = ResourceType.USER_ANALYTICS; // Define default values if not provided - fromTime = fromTime || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago - toTime = toTime || new Date(); // Default to now - granularity = granularity || RawAnalyticsGranularity.DAILY; // Default to daily granularity - metrics = metrics || Object.values(RawAnalyticsMetric); // Default to all metrics - showVerifiedFollowers = showVerifiedFollowers || true; // Default to true + fromTime = fromTime ?? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + toTime = toTime ?? new Date(); + granularity = granularity ?? RawAnalyticsGranularity.DAILY; + metrics = metrics ?? Object.values(RawAnalyticsMetric); + showVerifiedFollowers = showVerifiedFollowers ?? true; // Fetching raw analytics const response = await this.request(resource, { From 872b63b8539f5605c960866bfd475cdefb9da5b4 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Tue, 1 Jul 2025 15:10:09 +0000 Subject: [PATCH 040/119] Added CLI command for getting user analytics --- src/commands/User.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/commands/User.ts b/src/commands/User.ts index 4a57c6e6..8f7d5d4c 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -1,5 +1,6 @@ import { Command, createCommand } from 'commander'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics'; import { output } from '../helper/CliUtils'; import { Rettiwt } from '../Rettiwt'; @@ -28,6 +29,44 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Analytics + user.command('analytics') + .description('Fetch the analytics of the logged-in user (premium accounts only)') + .option('-f, --from-time ', 'The start time for fetching analytics') + .option('-t, --to-time ', 'The end time for fetching analytics') + .option( + '-g, --granularity ', + 'The granularity of the analytics data. Defaults to daily. Check https://rishikant181.github.io/Rettiwt-API/enums/RawAnalyticsGranularity.html for granularity options', + ) + .option( + '-m, --metrics ', + 'Comma-separated list of metrics required. Check https://rishikant181.github.io/Rettiwt-API/enums/RawAnalyticsMetric.html for available metrics', + ) + .option( + '-v, --verified-followers', + 'Whether to include verified follower count and relationship counts in the response. Defaults to true', + ) + .action(async (options?: UserAnalyticsOptions) => { + try { + const analytics = await rettiwt.user.analytics( + options?.fromTime ? new Date(options.fromTime) : undefined, + options?.toTime ? new Date(options.toTime) : undefined, + options?.granularity + ? RawAnalyticsGranularity[options.granularity as keyof typeof RawAnalyticsGranularity] + : undefined, + options?.metrics + ? options.metrics + .split(',') + .map((item) => RawAnalyticsMetric[item as keyof typeof RawAnalyticsMetric]) + : undefined, + options?.verifiedFollowers, + ); + output(analytics); + } catch (error) { + output(error); + } + }); + user.command('bookmarks') .description('Fetch your list of bookmarks') .argument('[count]', 'The number of bookmarks to fetch') @@ -224,4 +263,15 @@ function createUserCommand(rettiwt: Rettiwt): Command { return user; } +/** + * The options for fetching user analytics. + */ +type UserAnalyticsOptions = { + fromTime?: string; + toTime?: string; + granularity?: string; + metrics?: string; + verifiedFollowers?: boolean; +}; + export default createUserCommand; From 6ede776a1ec45960081cd2d5420b91fc3a44c4c2 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 1 Jul 2025 20:24:22 +0200 Subject: [PATCH 041/119] feat: raw responses types --- src/requests/DM.ts | 74 ++++---- src/types/raw/dm/Conversation.ts | 59 ++++++ src/types/raw/dm/InboxInitial.ts | 155 +++++++++++++++ src/types/raw/dm/InboxTimeline.ts | 301 ++++++++++++++++++++++++++++++ src/types/raw/dm/UserUpdates.ts | 46 +++++ 5 files changed, 598 insertions(+), 37 deletions(-) create mode 100644 src/types/raw/dm/Conversation.ts create mode 100644 src/types/raw/dm/InboxInitial.ts create mode 100644 src/types/raw/dm/InboxTimeline.ts create mode 100644 src/types/raw/dm/UserUpdates.ts diff --git a/src/requests/DM.ts b/src/requests/DM.ts index 79e1d0d4..1cc7b08e 100644 --- a/src/requests/DM.ts +++ b/src/requests/DM.ts @@ -28,7 +28,7 @@ const DM_BASE_PARAMS = { supports_edit: true, include_ext_edit_control: true, include_ext_business_affiliations_label: true, - ext: 'mediaColor%2CaltText%2CbusinessAffiliationsLabel%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle' + ext: 'mediaColor%2CaltText%2CbusinessAffiliationsLabel%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', /* eslint-enable @typescript-eslint/naming-convention */ }; @@ -46,7 +46,7 @@ const DM_USER_INCLUDE_PARAMS = { include_ext_is_blue_verified: 1, include_ext_verified_type: 1, include_ext_profile_image_shape: 1, - skip_status: 1 + skip_status: 1, /* eslint-enable @typescript-eslint/naming-convention */ }; @@ -56,31 +56,31 @@ const DM_USER_INCLUDE_PARAMS = { * @public */ export class DMRequests { - /** - * Get a specific DM conversation - * @param conversationId - The conversation ID (e.g., "394028042-1712730991884689408") - * @param maxId - Maximum ID for pagination (optional) - */ - public static conversation(conversationId: string, maxId?: string): AxiosRequestConfig { - const context = maxId ? 'FETCH_DM_CONVERSATION_HISTORY' : 'FETCH_DM_CONVERSATION'; - - return { - method: 'get', - url: `https://x.com/i/api/1.1/dm/conversation/${conversationId}.json`, - params: { - ...DM_BASE_PARAMS, - ...DM_USER_INCLUDE_PARAMS, - /* eslint-disable @typescript-eslint/naming-convention */ - max_id: maxId, - context: context, - dm_users: false, - include_conversation_info: true - /* eslint-enable @typescript-eslint/naming-convention */ - }, - paramsSerializer: { encode: encodeURIComponent }, - }; - } - + /** + * Get a specific DM conversation + * @param conversationId - The conversation ID (e.g., "394028042-1712730991884689408") + * @param maxId - Maximum ID for pagination (optional) + */ + public static conversation(conversationId: string, maxId?: string): AxiosRequestConfig { + const context = maxId ? 'FETCH_DM_CONVERSATION_HISTORY' : 'FETCH_DM_CONVERSATION'; + + return { + method: 'get', + url: `https://x.com/i/api/1.1/dm/conversation/${conversationId}.json`, + params: { + ...DM_BASE_PARAMS, + ...DM_USER_INCLUDE_PARAMS, + /* eslint-disable @typescript-eslint/naming-convention */ + max_id: maxId, + context: context, + dm_users: false, + include_conversation_info: true, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + /** * Get the initial state of the DM inbox */ @@ -94,13 +94,13 @@ export class DMRequests { /* eslint-disable @typescript-eslint/naming-convention */ dm_users: true, include_ext_parody_commentary_fan_label: true, - ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle' + ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', }, paramsSerializer: { encode: encodeURIComponent }, }; } - /** + /** * Get inbox timeline (pagination of conversations) * @param maxId - Maximum ID for pagination */ @@ -113,13 +113,13 @@ export class DMRequests { ...DM_USER_INCLUDE_PARAMS, /* eslint-disable @typescript-eslint/naming-convention */ max_id: maxId, - dm_users: false + dm_users: false, }, paramsSerializer: { encode: encodeURIComponent }, }; } - /** + /** * Create a new DM or get DM creation interface */ public static new(): AxiosRequestConfig { @@ -138,7 +138,7 @@ export class DMRequests { include_inbox_timelines: true, include_ext_media_color: true, supports_reactions: true, - supports_edit: true + supports_edit: true, }, paramsSerializer: { encode: encodeURIComponent }, }; @@ -161,7 +161,7 @@ export class DMRequests { }; } - /** + /** * Update the last seen event ID for a conversation * @param lastSeenEventId - The ID of the last seen event * @param trustedLastSeenEventId - The trusted last seen event ID (usually same as lastSeenEventId) @@ -173,12 +173,12 @@ export class DMRequests { data: qs.stringify({ /* eslint-disable @typescript-eslint/naming-convention */ last_seen_event_id: lastSeenEventId, - trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId + trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId, }), }; } - /** + /** * Get user updates for DMs (polling for new messages) * @param cursor - Cursor for pagination * @param activeConversationId - ID of the currently active conversation @@ -192,9 +192,9 @@ export class DMRequests { /* eslint-disable @typescript-eslint/naming-convention */ cursor: cursor, active_conversation_id: activeConversationId, - dm_users: false + dm_users: false, }, paramsSerializer: { encode: encodeURIComponent }, }; } -} \ No newline at end of file +} diff --git a/src/types/raw/dm/Conversation.ts b/src/types/raw/dm/Conversation.ts new file mode 100644 index 00000000..ef9aeb6b --- /dev/null +++ b/src/types/raw/dm/Conversation.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +import { Users, Conversations } from './InboxInitial'; + +/** + * The raw data received when fetching a specific conversation timeline. + * + * @public + */ +export interface IConversationTimelineResponse { + conversation_timeline: ConversationTimeline; +} + +interface ConversationTimeline { + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + max_entry_id: string; + entries: ConversationEntry[]; + users: Users; + conversations: Conversations; +} + +type ConversationEntry = { message: ConversationMessage } | { trust_conversation: TrustConversation }; + +interface ConversationMessage { + id: string; + time: string; + request_id: string; + conversation_id: string; + message_data: ConversationMessageData; +} + +interface ConversationMessageData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count?: number; + message_reactions?: MessageReaction[]; +} + +interface MessageReaction { + id: string; + time: string; + conversation_id: string; + message_id: string; + reaction_key: string; + emoji_reaction: string; + sender_id: string; +} + +interface TrustConversation { + id: string; + time: string; + request_id: string; + conversation_id: string; + reason: string; // e.g., "accept" +} diff --git a/src/types/raw/dm/InboxInitial.ts b/src/types/raw/dm/InboxInitial.ts new file mode 100644 index 00000000..0d9ae7c3 --- /dev/null +++ b/src/types/raw/dm/InboxInitial.ts @@ -0,0 +1,155 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching the initial state of the DM inbox. + * + * @public + */ +export interface IInboxInitialResponse { + inbox_initial_state: InboxInitialState; +} + +export interface InboxInitialState { + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; + cursor: string; + inbox_timelines: InboxTimelines; + entries: Entry[]; + users: Users; + conversations: Conversations; +} + +interface InboxTimelines { + trusted: TimelineStatus; + untrusted: TimelineStatus; + untrusted_low_quality: TimelineStatus; +} + +interface TimelineStatus { + status: string; // "HAS_MORE" | "AT_END" + min_entry_id: string; +} + +interface Entry { + message: Message; +} + +interface Message { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + message_data: MessageData; +} + +interface MessageData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count: number; +} + +export interface Users { + [userId: string]: User; +} + +interface User { + id: number; + id_str: string; + name: string; + screen_name: string; + profile_image_url: string; + profile_image_url_https: string; + following: boolean; + follow_request_sent: boolean; + description: string; + entities: UserEntities; + verified: boolean; + is_blue_verified: boolean; + protected: boolean; + blocking: boolean; + subscribed_by: boolean; + can_media_tag: boolean; + dm_blocked_by: boolean; + dm_blocking: boolean; + created_at: string; + friends_count: number; + followers_count: number; +} + +interface UserEntities { + url: UrlEntity; + description: DescriptionEntity; +} + +interface UrlEntity { + urls: UrlInfo[]; +} + +interface DescriptionEntity { + urls: UrlInfo[]; +} + +interface UrlInfo { + url: string; + expanded_url: string; + display_url: string; + indices: [number, number]; +} + +export interface Conversations { + [conversationId: string]: Conversation; +} + +interface Conversation { + conversation_id: string; + type: 'GROUP_DM' | 'ONE_TO_ONE'; + sort_event_id: string; + sort_timestamp: string; + participants: Participant[]; + nsfw: boolean; + notifications_disabled: boolean; + mention_notifications_disabled: boolean; + last_read_event_id: string; + trusted: boolean; + low_quality: boolean; + muted: boolean; + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + max_entry_id: string; + create_time?: string; // Only for GROUP_DM + created_by_user_id?: string; // Only for GROUP_DM + name?: string; // Only for GROUP_DM + avatar_image_https?: string; // Only for GROUP_DM + avatar?: ConversationAvatar; // Only for GROUP_DM + read_only?: boolean; // Only for ONE_TO_ONE + social_proof?: SocialProof[]; // Only for untrusted conversations +} + +interface ConversationAvatar { + image: { + original_info: { + url: string; + width: number; + height: number; + }; + }; +} + +interface Participant { + user_id: string; + join_time?: string; // Only for GROUP_DM + last_read_event_id?: string; + join_conversation_event_id?: string; // Only for GROUP_DM + is_admin?: boolean; // Only for GROUP_DM +} + +interface SocialProof { + proof_type: string; // e.g., "mutual_friends" + users: any[]; // Array of users (structure depends on proof_type) + total: number; +} diff --git a/src/types/raw/dm/InboxTimeline.ts b/src/types/raw/dm/InboxTimeline.ts new file mode 100644 index 00000000..a4bfa550 --- /dev/null +++ b/src/types/raw/dm/InboxTimeline.ts @@ -0,0 +1,301 @@ +/* eslint-disable */ + +import { Users, Conversations } from './InboxInitial'; + +/** + * The raw data received when fetching the inbox timeline. + * + * @public + */ +export interface IInboxTimelineResponse { + inbox_timeline: InboxTimeline; +} + +interface InboxTimeline { + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + entries: TimelineEntry[]; + users: Users; + conversations: Conversations; +} + +type TimelineEntry = + | { trust_conversation: TrustConversation } + | { message: TimelineMessage } + | { participants_leave: ParticipantsLeave }; + +interface TrustConversation { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + reason: string; // e.g., "accept" +} + +interface TimelineMessage { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + message_data: TimelineMessageData; +} + +interface TimelineMessageData { + id: string; + time: string; + recipient_id?: string; + sender_id: string; + conversation_id?: string; + text: string; + edit_count: number; + entities?: MessageEntities; + reply_data?: ReplyData; + attachment?: MessageAttachment; +} + +interface MessageEntities { + hashtags: any[]; + symbols: any[]; + user_mentions: UserMention[]; + urls: UrlEntity[]; +} + +interface UserMention { + screen_name: string; + name: string; + id: number; + id_str: string; + indices: [number, number]; +} + +interface UrlEntity { + url: string; + expanded_url: string; + display_url: string; + indices: [number, number]; +} + +interface ReplyData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count: number; + entities?: MessageEntities; +} + +interface MessageAttachment { + card?: CardAttachment; + tweet?: TweetAttachment; +} + +interface CardAttachment { + name: string; + url: string; + card_type_url: string; + binding_values: CardBindingValues; +} + +interface CardBindingValues { + vanity_url?: StringValue; + domain?: StringValue; + title?: StringValue; + description?: StringValue; + thumbnail_image_small?: ImageValue; + thumbnail_image?: ImageValue; + thumbnail_image_large?: ImageValue; + thumbnail_image_x_large?: ImageValue; + thumbnail_image_color?: ImageColorValue; + thumbnail_image_original?: ImageValue; + summary_photo_image_small?: ImageValue; + summary_photo_image?: ImageValue; + summary_photo_image_large?: ImageValue; + summary_photo_image_x_large?: ImageValue; + summary_photo_image_color?: ImageColorValue; + summary_photo_image_original?: ImageValue; + photo_image_full_size_small?: ImageValue; + photo_image_full_size?: ImageValue; + photo_image_full_size_large?: ImageValue; + photo_image_full_size_x_large?: ImageValue; + photo_image_full_size_color?: ImageColorValue; + photo_image_full_size_original?: ImageValue; + card_url?: StringValue; +} + +interface StringValue { + type: 'STRING'; + string_value: string; + scribe_key?: string; +} + +interface ImageValue { + type: 'IMAGE'; + image_value: { + url: string; + width: number; + height: number; + alt: string | null; + }; +} + +interface ImageColorValue { + type: 'IMAGE_COLOR'; + image_color_value: { + palette: ColorPalette[]; + }; +} + +interface ColorPalette { + percentage: number; + rgb: { + red: number; + green: number; + blue: number; + }; +} + +interface TweetAttachment { + id: string; + url: string; + display_url: string; + expanded_url: string; + indices: [number, number]; + status: TwitterStatus; +} + +interface TwitterStatus { + created_at: string; + id: number; + id_str: string; + full_text: string; + truncated: boolean; + display_text_range: [number, number]; + entities: MessageEntities; + source: string; + in_reply_to_status_id: number | null; + in_reply_to_status_id_str: string | null; + in_reply_to_user_id: number | null; + in_reply_to_user_id_str: string | null; + in_reply_to_screen_name: string | null; + user: TwitterUser; + geo: any; + coordinates: any; + place: any; + contributors: any; + is_quote_status: boolean; + retweet_count: number; + favorite_count: number; + reply_count: number; + quote_count: number; + favorited: boolean; + retweeted: boolean; + lang: string; + supplemental_language: string | null; + ext: TwitterExtensions; +} + +interface TwitterUser { + id: number; + id_str: string; + name: string; + screen_name: string; + location: string; + description: string; + url: string; + entities: UserEntityInfo; + protected: boolean; + followers_count: number; + fast_followers_count: number; + normal_followers_count: number; + friends_count: number; + listed_count: number; + created_at: string; + favourites_count: number; + utc_offset: any; + time_zone: any; + geo_enabled: boolean; + verified: boolean; + statuses_count: number; + media_count: number; + lang: any; + contributors_enabled: boolean; + is_translator: boolean; + is_translation_enabled: boolean; + profile_background_color: string; + profile_background_image_url: string | null; + profile_background_image_url_https: string | null; + profile_background_tile: boolean; + profile_image_url: string; + profile_image_url_https: string; + profile_banner_url: string; + profile_link_color: string; + profile_sidebar_border_color: string; + profile_sidebar_fill_color: string; + profile_text_color: string; + profile_use_background_image: boolean; + default_profile: boolean; + default_profile_image: boolean; + pinned_tweet_ids: number[]; + pinned_tweet_ids_str: string[]; + has_custom_timelines: boolean; + can_dm: any; + can_media_tag: boolean; + following: boolean; + follow_request_sent: boolean; + notifications: boolean; + muting: any; + blocking: boolean; + blocked_by: boolean; + want_retweets: boolean; + advertiser_account_type: string; + advertiser_account_service_levels: any[]; + business_profile_state: string; + translator_type: string; + withheld_in_countries: any[]; + followed_by: boolean; + ext: TwitterExtensions; + require_some_consent: boolean; +} + +interface UserEntityInfo { + url: { + urls: UrlEntity[]; + }; + description: { + urls: UrlEntity[]; + }; +} + +interface TwitterExtensions { + businessAffiliationsLabel?: { + r: { ok: any }; + ttl: number; + }; + superFollowMetadata?: { + r: { ok: any }; + ttl: number; + }; + parodyCommentaryFanLabel?: { + r: { ok: string }; + ttl: number; + }; + highlightedLabel?: { + r: { ok: any }; + ttl: number; + }; +} + +interface ParticipantsLeave { + id: string; + time: string; + affects_sort: boolean; + conversation_id: string; + participants: ParticipantInfo[]; +} + +interface ParticipantInfo { + user_id: string; +} diff --git a/src/types/raw/dm/UserUpdates.ts b/src/types/raw/dm/UserUpdates.ts new file mode 100644 index 00000000..d951d1d3 --- /dev/null +++ b/src/types/raw/dm/UserUpdates.ts @@ -0,0 +1,46 @@ +/* eslint-disable */ + +import { Users, Conversations, InboxInitialState } from './InboxInitial'; + +/** + * The raw data received when fetching user updates from the DM system. + * The response structure varies based on query parameters. + * + * @public + */ +export interface IUserUpdatesResponse { + user_events?: UserEvents; + inbox_initial_state?: InboxInitialState; +} + +/** + * User events can have different structures based on the request type: + * - With active_conversation_id + cursor: Full data with users and conversations + * - Without active_conversation_id and cursor: Same as inbox initial (see IInboxInitialResponse) + * - With cursor only: Minimal data with just event IDs and cursor + */ +type UserEvents = UserEventsWithData | UserEventsMinimal; + +/** + * Full user events data returned when requesting with active_conversation_id and cursor. + * Used for conversation-specific updates with user and conversation context. + */ +interface UserEventsWithData { + cursor: string; + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; + users: Users; + conversations: Conversations; +} + +/** + * Minimal user events data returned when requesting with cursor only (no active_conversation_id). + * Used for lightweight polling of event state without full data. + */ +interface UserEventsMinimal { + cursor: string; + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; +} From 87a8f801de5518fd11166ce931da17c3d3aaf1c1 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Wed, 2 Jul 2025 23:00:55 +0200 Subject: [PATCH 042/119] feat: Resource types & registration --- src/collections/Requests.ts | 6 ++++++ src/enums/Resource.ts | 6 ++++++ src/types/args/FetchArgs.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index d8f59048..e5a21158 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -1,6 +1,7 @@ import { AxiosRequestConfig } from 'axios'; import { ResourceType } from '../enums/Resource'; +import { DMRequests } from '../requests/DM'; import { ListRequests } from '../requests/List'; import { MediaRequests } from '../requests/Media'; import { TweetRequests } from '../requests/Tweet'; @@ -25,6 +26,11 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | MEDIA_UPLOAD_FINALIZE: (args: IPostArgs) => MediaRequests.finalizeUpload(args.upload!.id!), MEDIA_UPLOAD_INITIALIZE: (args: IPostArgs) => MediaRequests.initializeUpload(args.upload!.size!), + DM_CONVERSATION: (args: IFetchArgs) => DMRequests.conversation(args.conversationId!, args.maxId), + DM_INBOX_INITIAL_STATE: () => DMRequests.inboxInitial(), + DM_INBOX_TIMELINE: (args: IFetchArgs) => DMRequests.inboxTimeline(args.maxId), + DM_USER_UPDATES: (args: IFetchArgs) => DMRequests.userUpdates(args.cursor, args.activeConversationId), + TWEET_BOOKMARK: (args: IPostArgs) => TweetRequests.bookmark(args.id!), TWEET_DETAILS: (args: IFetchArgs) => TweetRequests.details(args.id!), TWEET_DETAILS_ALT: (args: IFetchArgs) => TweetRequests.replies(args.id!), diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index c02aad09..ae3b8754 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -13,6 +13,12 @@ export enum ResourceType { MEDIA_UPLOAD_FINALIZE = 'MEDIA_UPLOAD_FINALIZE', MEDIA_UPLOAD_INITIALIZE = 'MEDIA_UPLOAD_INITIALIZE', + // DM + DM_CONVERSATION = 'DM_CONVERSATION', + DM_INBOX_INITIAL_STATE = 'DM_INBOX_INITIAL_STATE', + DM_INBOX_TIMELINE = 'DM_INBOX_TIMELINE', + DM_USER_UPDATES = 'DM_USER_UPDATES', + // TWEET TWEET_BOOKMARK = 'TWEET_BOOKMARK', TWEET_DETAILS = 'TWEET_DETAILS', diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index 562068bb..225f7481 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -7,6 +7,31 @@ import { TweetRepliesSortType } from '../../enums/Tweet'; * @public */ export interface IFetchArgs { + + /** + * The id of the active conversation. + * + * @remarks + * - Required only for {@link ResourceType.DM_USER_UPDATES}. + */ + activeConversationId?: string; + + /** + * The maximum id of the data to fetch. + * + * @remarks + * - May be used for {@link ResourceType.DM_INBOX_TIMELINE} and {@link ResourceType.DM_CONVERSATION}. + */ + maxId?: string; + + /** + * The id of the conversation to fetch. + * + * @remarks + * - Required only for {@link ResourceType.DM_CONVERSATION}. + */ + conversationId?: string; + /** * The number of data items to fetch. * @@ -25,6 +50,8 @@ export interface IFetchArgs { */ count?: number; + + /** * The cursor to the batch of data to fetch. * From a43b08f442f947b479a453500ef5642f7256fa31 Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Thu, 3 Jul 2025 16:02:15 +0000 Subject: [PATCH 043/119] Fixed README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d256590f..d67c931d 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }) .then(data => { ... @@ -246,7 +246,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }, count, data.next.value) .then(data => { ... From f4aec5548b20f44baf77adb2eb6fca94575eb35c Mon Sep 17 00:00:00 2001 From: Rishikant181 Date: Thu, 3 Jul 2025 16:05:02 +0000 Subject: [PATCH 044/119] Fixed incorrect example in README --- README.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff741c50..21563d35 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }) .then(data => { ... @@ -243,7 +243,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }, count, data.next.value) .then(data => { ... diff --git a/package-lock.json b/package-lock.json index 34a8b669..83b642e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.4", + "version": "6.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.4", + "version": "6.0.5", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 89731af9..12548407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.4", + "version": "6.0.5", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 738e6bc348311181b512d498e14897cfb4efa94c Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Sat, 5 Jul 2025 19:18:43 +0000 Subject: [PATCH 045/119] Test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2015a890..7981be0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.5", + "version": "6.0.6", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 4cedd9f6e8c4994f7aa93901c07d8c7f36a370d3 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Sat, 5 Jul 2025 19:22:41 +0000 Subject: [PATCH 046/119] Test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7981be0b..2015a890 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.6", + "version": "6.0.5", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From c69c90f27986528332c9a188ae9d2015279b3490 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Sun, 6 Jul 2025 16:38:28 +0200 Subject: [PATCH 047/119] feat: extractor & data models & public service --- src/Rettiwt.ts | 5 + src/collections/Extractors.ts | 14 ++ src/enums/Data.ts | 1 + src/index.ts | 10 + src/models/data/Conversation.ts | 105 +++++++++ src/models/data/CursoredData.ts | 43 +++- src/models/data/DirectMessage.ts | 155 ++++++++++++++ src/services/public/DirectMessageService.ts | 223 ++++++++++++++++++++ src/types/data/Conversation.ts | 39 ++++ src/types/data/CursoredData.ts | 4 +- src/types/data/DirectMessage.ts | 33 +++ 11 files changed, 629 insertions(+), 3 deletions(-) create mode 100644 src/models/data/Conversation.ts create mode 100644 src/models/data/DirectMessage.ts create mode 100644 src/services/public/DirectMessageService.ts create mode 100644 src/types/data/Conversation.ts create mode 100644 src/types/data/DirectMessage.ts diff --git a/src/Rettiwt.ts b/src/Rettiwt.ts index 9e4c8548..1be5c6a5 100644 --- a/src/Rettiwt.ts +++ b/src/Rettiwt.ts @@ -1,4 +1,5 @@ import { RettiwtConfig } from './models/RettiwtConfig'; +import { DirectMessageService } from './services/public/DirectMessageService'; import { ListService } from './services/public/ListService'; import { TweetService } from './services/public/TweetService'; import { UserService } from './services/public/UserService'; @@ -49,6 +50,9 @@ export class Rettiwt { /** The configuration for Rettiwt. */ private _config: RettiwtConfig; + /** The instance used to fetch data related to direct messages. */ + public directMessage: DirectMessageService; + /** The instance used to fetch data related to lists. */ public list: ListService; @@ -65,6 +69,7 @@ export class Rettiwt { */ public constructor(config?: IRettiwtConfig) { this._config = new RettiwtConfig(config); + this.directMessage = new DirectMessageService(this._config); this.list = new ListService(this._config); this.tweet = new TweetService(this._config); this.user = new UserService(this._config); diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 1c2df6eb..2635a2e5 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,9 +1,14 @@ import { BaseType } from '../enums/Data'; import { Analytics } from '../models/data/Analytics'; import { CursoredData } from '../models/data/CursoredData'; +import { DirectMessage } from '../models/data/DirectMessage'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; import { User } from '../models/data/User'; +import { IConversationTimelineResponse } from '../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../types/raw/dm/InboxTimeline'; +import { IUserUpdatesResponse } from '../types/raw/dm/UserUpdates'; import { IListMembersResponse } from '../types/raw/list/Members'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; @@ -60,6 +65,15 @@ export const Extractors = { MEDIA_UPLOAD_INITIALIZE: (response: IMediaInitializeUploadResponse): string => response.media_id_string ?? undefined, + DM_CONVERSATION: (response: IConversationTimelineResponse): CursoredData => + new CursoredData(response, BaseType.DIRECT_MESSAGE), + DM_INBOX_INITIAL_STATE: (response: IInboxInitialResponse): CursoredData => + new CursoredData(response, BaseType.DIRECT_MESSAGE), + DM_INBOX_TIMELINE: (response: IInboxTimelineResponse): CursoredData => + new CursoredData(response, BaseType.DIRECT_MESSAGE), + DM_USER_UPDATES: (response: IUserUpdatesResponse): CursoredData => + new CursoredData(response, BaseType.DIRECT_MESSAGE), + TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => response?.data?.tweet_bookmark_put === 'Done', TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_ALT: (response: ITweetRepliesResponse, id: string): Tweet | undefined => Tweet.single(response, id), diff --git a/src/enums/Data.ts b/src/enums/Data.ts index 195a230e..29f98455 100644 --- a/src/enums/Data.ts +++ b/src/enums/Data.ts @@ -4,6 +4,7 @@ * @internal */ export enum BaseType { + DIRECT_MESSAGE = 'DIRECT_MESSAGE', NOTIFICATION = 'NOTIFICATION', TWEET = 'TWEET', USER = 'USER', diff --git a/src/index.ts b/src/index.ts index cbb7e0a3..1b5ed2e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,9 @@ export * from './enums/Tweet'; // MODELS export * from './models/args/FetchArgs'; export * from './models/args/PostArgs'; +export * from './models/data/Conversation'; export * from './models/data/CursoredData'; +export * from './models/data/DirectMessage'; export * from './models/data/List'; export * from './models/data/Notification'; export * from './models/data/Tweet'; @@ -26,12 +28,14 @@ export * from './models/data/User'; export * from './models/errors/TwitterError'; // REQUESTS +export * from './requests/DM'; export * from './requests/List'; export * from './requests/Media'; export * from './requests/Tweet'; export * from './requests/User'; // SERVICES +export * from './services/public/DirectMessageService'; export * from './services/public/FetcherService'; export * from './services/public/ListService'; export * from './services/public/TweetService'; @@ -40,7 +44,9 @@ export * from './services/public/UserService'; // TYPES export * from './types/args/FetchArgs'; export * from './types/args/PostArgs'; +export * from './types/data/Conversation'; export * from './types/data/CursoredData'; +export * from './types/data/DirectMessage'; export * from './types/data/List'; export * from './types/data/Notification'; export * from './types/data/Tweet'; @@ -102,3 +108,7 @@ export { IUserTweetsAndRepliesResponse as IRawUserTweetsAndRepliesResponse } fro export { IUserUnfollowResponse as IRawUserUnfollowResponse } from './types/raw/user/Unfollow'; export * from './types/ErrorHandler'; export * from './types/RettiwtConfig'; +export { IConversationTimelineResponse as IRawConversationTimelineResponse } from './types/raw/dm/Conversation'; +export { IInboxInitialResponse as IRawInboxInitialResponse } from './types/raw/dm/InboxInitial'; +export { IInboxTimelineResponse as IRawInboxTimelineResponse } from './types/raw/dm/InboxTimeline'; +export { IUserUpdatesResponse as IRawUserUpdatesResponse } from './types/raw/dm/UserUpdates'; diff --git a/src/models/data/Conversation.ts b/src/models/data/Conversation.ts new file mode 100644 index 00000000..d0ed3923 --- /dev/null +++ b/src/models/data/Conversation.ts @@ -0,0 +1,105 @@ +import { IConversation } from '../../types/data/Conversation'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; + +/** + * The details of a single conversation. + * + * @public + */ +export class Conversation implements IConversation { + /** The raw conversation details. */ + private readonly _raw: unknown; + + public avatarUrl?: string; + public hasMore: boolean; + public id: string; + public lastActivityAt: string; + public lastMessageId?: string; + public muted: boolean; + public name?: string; + public notificationsDisabled: boolean; + public participants: string[]; + public trusted: boolean; + public type: 'ONE_TO_ONE' | 'GROUP_DM'; + + /** + * @param conversation - The raw conversation details from the API response. + */ + public constructor(conversation: unknown) { + this._raw = { ...conversation as Record }; + + const conv = conversation as Record; + + this.id = conv.conversation_id && typeof conv.conversation_id === 'string' ? conv.conversation_id : ''; + this.type = (conv.type as 'ONE_TO_ONE' | 'GROUP_DM') || 'ONE_TO_ONE'; + + const participants = conv.participants as Array> | undefined; + this.participants = participants?.map((p) => p.user_id && typeof p.user_id === 'string' ? p.user_id : '') || []; + + this.name = conv.name && typeof conv.name === 'string' ? conv.name : undefined; + + const avatar = conv.avatar as Record | undefined; + const image = avatar?.image as Record | undefined; + const originalInfo = image?.original_info as Record | undefined; + this.avatarUrl = (conv.avatar_image_https && typeof conv.avatar_image_https === 'string') + ? conv.avatar_image_https + : (originalInfo?.url && typeof originalInfo.url === 'string') + ? originalInfo.url + : undefined; + + this.trusted = Boolean(conv.trusted); + this.muted = Boolean(conv.muted); + this.notificationsDisabled = Boolean(conv.notifications_disabled); + this.lastActivityAt = conv.sort_timestamp + ? new Date(Number(conv.sort_timestamp)).toISOString() + : new Date().toISOString(); + this.lastMessageId = conv.sort_event_id && typeof conv.sort_event_id === 'string' ? conv.sort_event_id : undefined; + this.hasMore = conv.status === 'HAS_MORE'; + } + + /** The raw conversation details. */ + public get raw(): unknown { + return { ...this._raw as Record }; + } + + /** + * Extracts and deserializes the list of conversations from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized list of conversations. + */ + public static list(response: NonNullable): Conversation[] { + const conversations: Conversation[] = []; + + // Handle inbox initial state response + const inboxResponse = response as IInboxInitialResponse; + if (inboxResponse.inbox_initial_state?.conversations) { + const rawConversations = inboxResponse.inbox_initial_state.conversations; + for (const [, conversation] of Object.entries(rawConversations)) { + conversations.push(new Conversation(conversation)); + } + } + + return conversations; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IConversation { + return { + avatarUrl: this.avatarUrl, + hasMore: this.hasMore, + id: this.id, + lastActivityAt: this.lastActivityAt, + lastMessageId: this.lastMessageId, + muted: this.muted, + name: this.name, + notificationsDisabled: this.notificationsDisabled, + participants: this.participants, + trusted: this.trusted, + type: this.type, + }; + } +} \ No newline at end of file diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index b70bfd7f..ebc36290 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -5,6 +5,8 @@ import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; +import { Conversation } from './Conversation'; +import { DirectMessage } from './DirectMessage'; import { Notification } from './Notification'; import { Tweet } from './Tweet'; import { User } from './User'; @@ -16,7 +18,7 @@ import { User } from './User'; * * @public */ -export class CursoredData implements ICursoredData { +export class CursoredData implements ICursoredData { public list: T[]; public next: string; @@ -29,7 +31,11 @@ export class CursoredData implements ICur this.list = []; this.next = ''; - if (type == BaseType.TWEET) { + if (type == BaseType.DIRECT_MESSAGE) { + this.list = DirectMessage.list(response) as T[]; + // For DM responses, we need to extract cursor differently depending on the response type + this.next = this._extractDMCursor(response); + } else if (type == BaseType.TWEET) { this.list = Tweet.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; } else if (type == BaseType.USER) { @@ -41,6 +47,39 @@ export class CursoredData implements ICur } } + /** + * Extract cursor from DM responses which have different structures. + */ + private _extractDMCursor(response: NonNullable): string { + const resp = response as Record; + + // Check for inbox_initial_state cursor + const inboxState = resp.inbox_initial_state as Record | undefined; + if (inboxState?.cursor && typeof inboxState.cursor === 'string') { + return inboxState.cursor; + } + + // Check for conversation_timeline min_entry_id for pagination + const conversationTimeline = resp.conversation_timeline as Record | undefined; + if (conversationTimeline?.min_entry_id && typeof conversationTimeline.min_entry_id === 'string') { + return conversationTimeline.min_entry_id; + } + + // Check for inbox_timeline min_entry_id for pagination + const inboxTimeline = resp.inbox_timeline as Record | undefined; + if (inboxTimeline?.min_entry_id && typeof inboxTimeline.min_entry_id === 'string') { + return inboxTimeline.min_entry_id; + } + + // Check for user_events cursor + const userEvents = resp.user_events as Record | undefined; + if (userEvents?.cursor && typeof userEvents.cursor === 'string') { + return userEvents.cursor; + } + + return ''; + } + /** * @returns A serializable JSON representation of `this` object. */ diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts new file mode 100644 index 00000000..b47e9bca --- /dev/null +++ b/src/models/data/DirectMessage.ts @@ -0,0 +1,155 @@ +import { IDirectMessage } from '../../types/data/DirectMessage'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +/** + * The details of a single direct message. + * + * @public + */ +export class DirectMessage implements IDirectMessage { + /** The raw message details. */ + private readonly _raw: unknown; + + public conversationId: string; + public createdAt: string; + public editCount?: number; + public id: string; + public mediaUrls?: string[]; + public read?: boolean; + public recipientId?: string; + public senderId: string; + public text: string; + + /** + * @param message - The raw message details from the API response. + */ + public constructor(message: unknown) { + this._raw = { ...message as Record }; + + const msg = message as Record; + const messageData = msg.message_data as Record | undefined; + + this.id = String((messageData?.id && typeof messageData.id === 'string' ? messageData.id : '') || + (msg.id && typeof msg.id === 'string' ? msg.id : '') || ''); + this.conversationId = String((msg.conversation_id && typeof msg.conversation_id === 'string' ? msg.conversation_id : '') || + (messageData?.conversation_id && typeof messageData.conversation_id === 'string' ? messageData.conversation_id : '') || ''); + this.senderId = String((messageData?.sender_id && typeof messageData.sender_id === 'string' ? messageData.sender_id : '') || + (msg.sender_id && typeof msg.sender_id === 'string' ? msg.sender_id : '') || ''); + this.recipientId = (messageData?.recipient_id && typeof messageData.recipient_id === 'string' ? messageData.recipient_id : undefined) || + (msg.recipient_id && typeof msg.recipient_id === 'string' ? msg.recipient_id : undefined); + this.text = String((messageData?.text && typeof messageData.text === 'string' ? messageData.text : '') || + (msg.text && typeof msg.text === 'string' ? msg.text : '') || ''); + this.createdAt = msg.time + ? new Date(Number(msg.time)).toISOString() + : messageData?.time + ? new Date(Number(messageData.time)).toISOString() + : new Date().toISOString(); + this.editCount = Number(messageData?.edit_count) || 0; + this.mediaUrls = this._extractMediaUrls(message); + this.read = true; // Default to true, can be enhanced later + } + + /** The raw message details. */ + public get raw(): unknown { + return { ...this._raw as Record }; + } + + /** + * Extract media URLs from message attachment data. + */ + private _extractMediaUrls(message: unknown): string[] | undefined { + const urls: string[] = []; + const msg = message as Record; + const messageData = msg.message_data as Record | undefined; + + // Check for card attachments with images + const attachment = messageData?.attachment as Record | undefined; + const card = attachment?.card as Record | undefined; + const bindingValues = card?.binding_values as Record | undefined; + + if (bindingValues) { + const thumbnailImage = bindingValues.thumbnail_image as Record | undefined; + const photoImageFullSize = bindingValues.photo_image_full_size as Record | undefined; + + if (thumbnailImage?.image_value) { + const imageValue = thumbnailImage.image_value as Record; + if (imageValue.url && typeof imageValue.url === 'string') { + urls.push(imageValue.url); + } + } + if (photoImageFullSize?.image_value) { + const imageValue = photoImageFullSize.image_value as Record; + if (imageValue.url && typeof imageValue.url === 'string') { + urls.push(imageValue.url); + } + } + } + + return urls.length > 0 ? urls : undefined; + } + + /** + * Extracts and deserializes the list of direct messages from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized list of direct messages. + */ + public static list(response: NonNullable): DirectMessage[] { + const messages: DirectMessage[] = []; + + // Handle inbox initial state response + const inboxResponse = response as IInboxInitialResponse; + if (inboxResponse.inbox_initial_state?.entries) { + const entries = inboxResponse.inbox_initial_state.entries; + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + } + + // Handle conversation timeline response + const conversationResponse = response as IConversationTimelineResponse; + if (conversationResponse.conversation_timeline?.entries) { + const entries = conversationResponse.conversation_timeline.entries; + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + } + + // Handle inbox timeline response + const timelineResponse = response as IInboxTimelineResponse; + if (timelineResponse.inbox_timeline?.entries) { + const entries = timelineResponse.inbox_timeline.entries; + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + } + + return messages; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IDirectMessage { + return { + conversationId: this.conversationId, + createdAt: this.createdAt, + editCount: this.editCount, + id: this.id, + mediaUrls: this.mediaUrls, + read: this.read, + recipientId: this.recipientId, + senderId: this.senderId, + text: this.text, + }; + } +} \ No newline at end of file diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts new file mode 100644 index 00000000..e0e3e2d3 --- /dev/null +++ b/src/services/public/DirectMessageService.ts @@ -0,0 +1,223 @@ +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; +import { CursoredData } from '../../models/data/CursoredData'; +import { DirectMessage } from '../../models/data/DirectMessage'; +import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { FetcherService } from './FetcherService'; + +/** + * Handles interacting with resources related to direct messages + * + * @public + */ +export class DirectMessageService extends FetcherService { + /** + * @param config - The config object for configuring the Rettiwt instance. + * + * @internal + */ + public constructor(config: RettiwtConfig) { + super(config); + } + + /** + * Get the full conversation history for a specific conversation. + * Use this to load complete message history for a conversation identified from the inbox. + * + * @param conversationId - The ID of the conversation (e.g., "394028042-1712730991884689408"). + * @param cursor - The cursor for pagination (maxId from previous response). + * + * @returns The conversation timeline with messages. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching a specific conversation + * rettiwt.dm.conversation('394028042-1712730991884689408') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async conversation(conversationId: string, cursor?: string): Promise> { + const resource = ResourceType.DM_CONVERSATION; + + // Fetching raw conversation timeline + const response = await this.request(resource, { + conversationId: conversationId, + maxId: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the initial state of the DM inbox, including recent conversations and messages. + * This is the main entry point for the DM system following the "Inbox as Entry Point" pattern. + * + * @returns The initial DM inbox state with recent messages and conversations. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the initial DM inbox state + * rettiwt.dm.inbox() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async inbox(): Promise> { + const resource = ResourceType.DM_INBOX_INITIAL_STATE; + + // Fetching raw inbox initial state + const response = await this.request(resource, {}); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + + + /** + * Get more conversations from the inbox timeline (for pagination). + * Use this to load older conversations beyond what's included in the initial inbox state. + * + * @param cursor - The cursor to the batch of conversations to fetch (maxId from previous response). + * + * @returns The inbox timeline with older conversations. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching older conversations using pagination + * rettiwt.dm.inboxTimeline('1803853649426133349') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async inboxTimeline(cursor?: string): Promise> { + const resource = ResourceType.DM_INBOX_TIMELINE; + + // Fetching raw inbox timeline + const response = await this.request(resource, { + maxId: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + + + /** + * Stream new DM updates in pseudo real-time by polling the user updates endpoint. + * This can be used to detect new messages and conversation changes. + * + * @param pollingInterval - The interval in milliseconds to poll for new updates. Default interval is 30000 ms (30 seconds). + * @param activeConversationId - ID of the currently active conversation for context-aware updates. + * + * @returns An async generator that yields new messages as they are received. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Creating a function that streams all new DM updates + * async function streamDMUpdates() { + * try { + * // Awaiting for the messages returned by the AsyncGenerator + * for await (const message of rettiwt.dm.streamUpdates(10000)) { + * console.log(`New message: ${message.text}`); + * } + * } + * catch (err) { + * console.log(err); + * } + * } + * + * // Calling the function + * streamDMUpdates(); + * ``` + */ + public async *streamUpdates(pollingInterval = 30000, activeConversationId?: string): AsyncGenerator { + const resource = ResourceType.DM_USER_UPDATES; + + /** Whether it's the first batch of updates or not. */ + let first = true; + + /** The cursor to the last update received. */ + let cursor: string | undefined = undefined; + + while (true) { + // Pause execution for the specified polling interval before proceeding to the next iteration + await new Promise((resolve) => setTimeout(resolve, pollingInterval)); + + // Get the batch of updates after the given cursor + const response = await this.request(resource, { + cursor: cursor, + activeConversationId: activeConversationId, + }); + + // Deserializing response + const updates = Extractors[resource](response); + + // Sorting the messages by time, from oldest to recent + updates.list.sort((a, b) => new Date(a.createdAt).valueOf() - new Date(b.createdAt).valueOf()); + + // If not first batch, return new messages + if (!first) { + // Yield the new messages + for (const message of updates.list) { + yield message; + } + } + // Else do nothing since first batch contains messages that have already been received + else { + first = false; + } + + cursor = updates.next; + } + } +} \ No newline at end of file diff --git a/src/types/data/Conversation.ts b/src/types/data/Conversation.ts new file mode 100644 index 00000000..702f3e2a --- /dev/null +++ b/src/types/data/Conversation.ts @@ -0,0 +1,39 @@ +/** + * The details of a single conversation. + * + * @public + */ +export interface IConversation { + /** The unique identifier of the conversation. */ + id: string; + + /** The type of conversation (ONE_TO_ONE or GROUP_DM). */ + type: 'ONE_TO_ONE' | 'GROUP_DM'; + + /** Array of participant user IDs. */ + participants: string[]; + + /** The name of the conversation (for group DMs). */ + name?: string; + + /** URL to the conversation avatar (for group DMs). */ + avatarUrl?: string; + + /** Whether the conversation is trusted. */ + trusted: boolean; + + /** Whether the conversation is muted. */ + muted: boolean; + + /** Whether notifications are disabled. */ + notificationsDisabled: boolean; + + /** The timestamp of the last activity (ISO 8601 format). */ + lastActivityAt: string; + + /** The ID of the last message. */ + lastMessageId?: string; + + /** Whether there are more messages to load. */ + hasMore: boolean; +} \ No newline at end of file diff --git a/src/types/data/CursoredData.ts b/src/types/data/CursoredData.ts index d2dc032d..2f2c59af 100644 --- a/src/types/data/CursoredData.ts +++ b/src/types/data/CursoredData.ts @@ -1,3 +1,5 @@ +import { IConversation } from './Conversation'; +import { IDirectMessage } from './DirectMessage'; import { INotification } from './Notification'; import { ITweet } from './Tweet'; import { IUser } from './User'; @@ -9,7 +11,7 @@ import { IUser } from './User'; * * @public */ -export interface ICursoredData { +export interface ICursoredData { /** The batch of data of the given type. */ list: T[]; diff --git a/src/types/data/DirectMessage.ts b/src/types/data/DirectMessage.ts new file mode 100644 index 00000000..29826c98 --- /dev/null +++ b/src/types/data/DirectMessage.ts @@ -0,0 +1,33 @@ +/** + * The details of a single direct message. + * + * @public + */ +export interface IDirectMessage { + /** The unique identifier of the message. */ + id: string; + + /** The ID of the conversation this message belongs to. */ + conversationId: string; + + /** The ID of the user who sent the message. */ + senderId: string; + + /** The ID of the user who received the message (for one-to-one conversations). */ + recipientId?: string; + + /** The text content of the message. */ + text: string; + + /** The timestamp when the message was sent (ISO 8601 format). */ + createdAt: string; + + /** Array of media URLs attached to the message. */ + mediaUrls?: string[]; + + /** Number of times the message has been edited. */ + editCount?: number; + + /** Whether the message has been read. */ + read?: boolean; +} \ No newline at end of file From d1a3cfd0a5e1b9b2722a5968a2405a2da1ca6fa9 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 10 Jul 2025 16:10:47 +0000 Subject: [PATCH 048/119] Added raw requests and types for adding and removing members from a list --- src/enums/Resource.ts | 2 + src/requests/List.ts | 58 ++++++++++ src/types/raw/list/AddMember.ts | 175 +++++++++++++++++++++++++++++ src/types/raw/list/RemoveMember.ts | 174 ++++++++++++++++++++++++++++ 4 files changed, 409 insertions(+) create mode 100644 src/types/raw/list/AddMember.ts create mode 100644 src/types/raw/list/RemoveMember.ts diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index c02aad09..7c68bbe4 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -5,6 +5,8 @@ */ export enum ResourceType { // LIST + LIST_MEMBER_ADD = 'LIST_MEMBER_ADD', + LIST_MEMBER_REMOVE = 'LIST_MEMBER_REMOVE', LIST_MEMBERS = 'LIST_MEMBERS', LIST_TWEETS = 'LIST_TWEETS', diff --git a/src/requests/List.ts b/src/requests/List.ts index 51aa19cf..6493bce4 100644 --- a/src/requests/List.ts +++ b/src/requests/List.ts @@ -6,6 +6,35 @@ import { AxiosRequestConfig } from 'axios'; * @public */ export class ListRequests { + /** + * @param listId - The ID of the target list. + * @param userId - The ID of the user to be added as a member. + */ + public static addMember(listId: string, userId: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/uFQumgzNDR27zs0yK5J3Fw/ListAddMember', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + + variables: { + listId: listId, + userId: userId, + }, + features: { + payments_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: false, + rweb_tipjar_consumption_enabled: false, + verified_phone_label_enabled: false, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: false, + }, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the list whose details are to be fetched. */ @@ -83,6 +112,35 @@ export class ListRequests { }; } + /** + * @param listId - The ID of the target list. + * @param userId - The ID of the user to remove as a member. + */ + public static removeMember(listId: string, userId: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/IzgPnK3wZpNgpcN31ry3Xg/ListRemoveMember', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + + variables: { + listId: listId, + userId: userId, + }, + features: { + payments_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: false, + rweb_tipjar_consumption_enabled: false, + verified_phone_label_enabled: false, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: false, + }, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the list whose tweets are to be fetched. * @param count - The number of tweets to fetch. Must be \<= 100. diff --git a/src/types/raw/list/AddMember.ts b/src/types/raw/list/AddMember.ts new file mode 100644 index 00000000..45ad8e8a --- /dev/null +++ b/src/types/raw/list/AddMember.ts @@ -0,0 +1,175 @@ +/* eslint-disable */ + +/** + * The raw data received after adding a member to a tweet list. + * + * @public + */ +export interface IListMemberAddResponse { + data: Data; +} + +export interface Data { + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: any[]; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + members_context: string; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +export interface Entities { + description: Description; +} + +export interface Description { + urls: any[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings {} + +export interface Verification { + verified: boolean; +} diff --git a/src/types/raw/list/RemoveMember.ts b/src/types/raw/list/RemoveMember.ts new file mode 100644 index 00000000..1d8c0b68 --- /dev/null +++ b/src/types/raw/list/RemoveMember.ts @@ -0,0 +1,174 @@ +/* eslint-disable */ + +/** + * The raw data received after removing a member from a tweet list. + * + * @public + */ +export interface IListMemberRemoveResponse { + data: Data; +} + +export interface Data { + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: any[]; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +export interface Entities { + description: Description; +} + +export interface Description { + urls: any[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings {} + +export interface Verification { + verified: boolean; +} From 3012d9fc61a028bd207b63d2d011006e9de9b08b Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 10 Jul 2025 16:34:34 +0000 Subject: [PATCH 049/119] Added ability to add and remove members from a list --- src/collections/Extractors.ts | 6 +++ src/collections/Groups.ts | 2 + src/collections/Requests.ts | 2 + src/commands/List.ts | 32 +++++++++++- src/models/args/PostArgs.ts | 2 + src/services/public/ListService.ts | 84 ++++++++++++++++++++++++++++++ src/types/args/PostArgs.ts | 10 ++++ 7 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 1c2df6eb..864b530e 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -4,7 +4,9 @@ import { CursoredData } from '../models/data/CursoredData'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; import { User } from '../models/data/User'; +import { IListMemberAddResponse } from '../types/raw/list/AddMember'; import { IListMembersResponse } from '../types/raw/list/Members'; +import { IListMemberRemoveResponse } from '../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; import { ITweetBookmarkResponse } from '../types/raw/tweet/Bookmark'; @@ -52,6 +54,10 @@ export const Extractors = { LIST_MEMBERS: (response: IListMembersResponse): CursoredData => new CursoredData(response, BaseType.USER), + LIST_MEMBER_ADD: (response: IListMemberAddResponse): number | undefined => + response.data?.list?.member_count ?? undefined, + LIST_MEMBER_REMOVE: (response: IListMemberRemoveResponse): number | undefined => + response.data?.list?.member_count ?? undefined, LIST_TWEETS: (response: IListTweetsResponse): CursoredData => new CursoredData(response, BaseType.TWEET), diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 221362af..93d094b9 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -51,6 +51,8 @@ export const FetchResourcesGroup = [ * @internal */ export const PostResourcesGroup = [ + ResourceType.LIST_MEMBER_ADD, + ResourceType.LIST_MEMBER_REMOVE, ResourceType.MEDIA_UPLOAD_APPEND, ResourceType.MEDIA_UPLOAD_FINALIZE, ResourceType.MEDIA_UPLOAD_INITIALIZE, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index d8f59048..fe2cc715 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -19,6 +19,8 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | /* eslint-disable @typescript-eslint/naming-convention */ LIST_MEMBERS: (args: IFetchArgs) => ListRequests.members(args.id!, args.count, args.cursor), + LIST_MEMBER_ADD: (args: IPostArgs) => ListRequests.addMember(args.id!, args.userId!), + LIST_MEMBER_REMOVE: (args: IPostArgs) => ListRequests.removeMember(args.id!, args.userId!), LIST_TWEETS: (args: IFetchArgs) => ListRequests.tweets(args.id!, args.count, args.cursor), MEDIA_UPLOAD_APPEND: (args: IPostArgs) => MediaRequests.appendUpload(args.upload!.id!, args.upload!.media!), diff --git a/src/commands/List.ts b/src/commands/List.ts index 52c59813..d91ef51d 100644 --- a/src/commands/List.ts +++ b/src/commands/List.ts @@ -13,10 +13,24 @@ function createListCommand(rettiwt: Rettiwt): Command { // Creating the 'list' command const list = createCommand('list').description('Access resources releated to lists'); + // Add member + list.command('add-member') + .description('Add a new member to a list') + .argument('', 'The ID of the tweet list') + .argument('', 'The ID of the user to add') + .action(async (listId: string, userId: string) => { + try { + const memberCount = await rettiwt.list.addMember(listId, userId); + output(memberCount); + } catch (error) { + output(error); + } + }); + // Members list.command('members') .description('Fetch the list of members of the given tweet list') - .argument('', 'The id of the tweet list') + .argument('', 'The ID of the tweet list') .argument('[count]', 'The number of members to fetch') .argument('[cursor]', 'The cursor to the batch of members to fetch') .action(async (id: string, count?: string, cursor?: string) => { @@ -28,10 +42,24 @@ function createListCommand(rettiwt: Rettiwt): Command { } }); + // Remove member + list.command('remove-member') + .description('Remove a new member from a list') + .argument('', 'The ID of the tweet list') + .argument('', 'The ID of the user to remove') + .action(async (listId: string, userId: string) => { + try { + const memberCount = await rettiwt.list.removeMember(listId, userId); + output(memberCount); + } catch (error) { + output(error); + } + }); + // Tweets list.command('tweets') .description('Fetch the list of tweets in the tweet list with the given id') - .argument('', 'The id of the tweet list') + .argument('', 'The ID of the tweet list') .argument('[count]', 'The number of tweets to fetch') .argument('[cursor]', 'The cursor to the batch of tweets to fetch') .action(async (id: string, count?: string, cursor?: string) => { diff --git a/src/models/args/PostArgs.ts b/src/models/args/PostArgs.ts index ff30779c..436df5d2 100644 --- a/src/models/args/PostArgs.ts +++ b/src/models/args/PostArgs.ts @@ -9,6 +9,7 @@ export class PostArgs implements IPostArgs { public id?: string; public tweet?: NewTweet; public upload?: UploadArgs; + public userId?: string; /** * @param resource - The resource to be posted. @@ -18,6 +19,7 @@ export class PostArgs implements IPostArgs { this.id = args.id; this.tweet = args.tweet ? new NewTweet(args.tweet) : undefined; this.upload = args.upload ? new UploadArgs(args.upload) : undefined; + this.userId = args.userId; } } diff --git a/src/services/public/ListService.ts b/src/services/public/ListService.ts index 47d3277e..35caf913 100644 --- a/src/services/public/ListService.ts +++ b/src/services/public/ListService.ts @@ -4,7 +4,9 @@ import { CursoredData } from '../../models/data/CursoredData'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IListMemberAddResponse } from '../../types/raw/list/AddMember'; import { IListMembersResponse } from '../../types/raw/list/Members'; +import { IListMemberRemoveResponse } from '../../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../../types/raw/list/Tweets'; import { FetcherService } from './FetcherService'; @@ -19,6 +21,47 @@ export class ListService extends FetcherService { super(config); } + /** + * Add a user as a member of a list. + * + * @param listId - The ID of the target list. + * @param userId - The ID of the target user to be added as a member. + * + * @returns The new member count of the list. If adding was unsuccessful, return `undefined`. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Adding a user with ID '123456789' as a member to the list with ID '987654321' + * rettiwt.list.addMember('987654321', '123456789') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async addMember(listId: string, userId: string): Promise { + const resource: ResourceType = ResourceType.LIST_MEMBER_ADD; + + // Adding the user as a member + const response = await this.request(resource, { + id: listId, + userId: userId, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + /** * Get the list of members of a tweet list. * @@ -64,6 +107,47 @@ export class ListService extends FetcherService { return data; } + /** + * Remove a member from a list. + * + * @param listId - The ID of the target list. + * @param userId - The ID of the target user to removed from the members. + * + * @returns The new member count of the list. If removal was unsuccessful, return `undefined`. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Removing a user with ID '123456789' from the member of the list with ID '987654321' + * rettiwt.list.removeMember('987654321', '123456789') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async removeMember(listId: string, userId: string): Promise { + const resource: ResourceType = ResourceType.LIST_MEMBER_REMOVE; + + // Removing the member + const response = await this.request(resource, { + id: listId, + userId: userId, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + /** * Get the list of tweets from a tweet list. * diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index 4930f5f4..3960ce2c 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -39,6 +39,16 @@ export interface IPostArgs { * - {@link ResourceType.MEDIA_UPLOAD_INITIALIZE} */ upload?: IUploadArgs; + + /** + * The id of the target user. + * + * @remarks + * Required only for the following resources: + * - {@link ResourceType.LIST_MEMBER_ADD} + * - {@link ResourceType.LIST_MEMBER_REMOVE} + */ + userId?: string; } /** From a346e2f92ee06e10f97157fa14a1387d0ebe31a6 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 10 Jul 2025 16:38:16 +0000 Subject: [PATCH 050/119] Updated README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d67c931d..3a67397a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - 'User' authentication (logging in) grants access to the following resources/actions: + - List Add Member - List Members + - List Remove Member - List Tweets - Tweet Details - Single and Bulk - Tweet Bookmark @@ -439,7 +441,9 @@ So far, the following operations are supported: ### List +- [Adding a member to a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#addMember) - [Getting the members of a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#members) +- [Removing a member from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#removeMember) - [Getting the list of tweets from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#tweets) ### Tweets From 7bb0317a8e3f9c4a3d2eb57b1addda64af884f8e Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 10 Jul 2025 16:43:20 +0000 Subject: [PATCH 051/119] Updated exports --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index cbb7e0a3..ad9e2f9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,8 @@ export { IDataResult as IRawDataResult } from './types/raw/composite/DataResult' export { ITimelineTweet as IRawTimelineTweet } from './types/raw/composite/TimelineTweet'; export { ITimelineUser as IRawTimelineUser } from './types/raw/composite/TimelineUser'; export { IResponse as IRawResponse } from './types/raw/generic/Response'; +export { IListMemberAddResponse as IRawListMemberAddResponse } from './types/raw/list/AddMember'; +export { IListMemberRemoveResponse as IRawListMemberRemoveResponse } from './types/raw/list/RemoveMember'; export { IListDetailsResponse as IRawListDetailsResponse } from './types/raw/list/Details'; export { IListMembersResponse as IRawListMembersResponse } from './types/raw/list/Members'; export { IListTweetsResponse as IRawListTweetsResponse } from './types/raw/list/Tweets'; From e8ceffb116aca4237b5ced76c5f652349d378987 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 11 Jul 2025 14:35:43 +0200 Subject: [PATCH 052/119] refactor: inbox and conv data models --- src/collections/Extractors.ts | 16 +- src/collections/Groups.ts | 4 + src/collections/Requests.ts | 4 +- src/enums/Resource.ts | 2 +- src/index.ts | 4 +- src/models/args/FetchArgs.ts | 6 + src/models/data/Conversation.ts | 309 +++++++++++++++-- src/models/data/CursoredData.ts | 45 +-- src/models/data/DirectMessage.ts | 366 ++++++++++++++++---- src/models/data/Inbox.ts | 124 +++++++ src/requests/{DM.ts => DirectMessage.ts} | 147 ++++---- src/services/public/DirectMessageService.ts | 154 +++----- src/types/args/FetchArgs.ts | 7 +- src/types/args/PostArgs.ts | 8 + src/types/data/Conversation.ts | 7 +- src/types/data/DirectMessage.ts | 2 +- src/types/data/Inbox.ts | 23 ++ src/types/raw/dm/InboxInitial.ts | 4 +- 18 files changed, 910 insertions(+), 322 deletions(-) create mode 100644 src/models/data/Inbox.ts rename src/requests/{DM.ts => DirectMessage.ts} (64%) create mode 100644 src/types/data/Inbox.ts diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 2635a2e5..b600ce7f 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,14 +1,14 @@ import { BaseType } from '../enums/Data'; import { Analytics } from '../models/data/Analytics'; +import { Conversation } from '../models/data/Conversation'; import { CursoredData } from '../models/data/CursoredData'; -import { DirectMessage } from '../models/data/DirectMessage'; +import { Inbox } from '../models/data/Inbox'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; import { User } from '../models/data/User'; import { IConversationTimelineResponse } from '../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../types/raw/dm/InboxTimeline'; -import { IUserUpdatesResponse } from '../types/raw/dm/UserUpdates'; import { IListMembersResponse } from '../types/raw/list/Members'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; @@ -65,14 +65,10 @@ export const Extractors = { MEDIA_UPLOAD_INITIALIZE: (response: IMediaInitializeUploadResponse): string => response.media_id_string ?? undefined, - DM_CONVERSATION: (response: IConversationTimelineResponse): CursoredData => - new CursoredData(response, BaseType.DIRECT_MESSAGE), - DM_INBOX_INITIAL_STATE: (response: IInboxInitialResponse): CursoredData => - new CursoredData(response, BaseType.DIRECT_MESSAGE), - DM_INBOX_TIMELINE: (response: IInboxTimelineResponse): CursoredData => - new CursoredData(response, BaseType.DIRECT_MESSAGE), - DM_USER_UPDATES: (response: IUserUpdatesResponse): CursoredData => - new CursoredData(response, BaseType.DIRECT_MESSAGE), + DM_CONVERSATION: (response: IConversationTimelineResponse): Conversation | undefined => + Conversation.fromConversationTimeline(response), + DM_INBOX_INITIAL_STATE: (response: IInboxInitialResponse): Inbox => new Inbox(response), + DM_INBOX_TIMELINE: (response: IInboxTimelineResponse): Inbox => new Inbox(response), TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => response?.data?.tweet_bookmark_put === 'Done', TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 221362af..27211672 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -19,6 +19,9 @@ export const AllowGuestAuthenticationGroup = [ export const FetchResourcesGroup = [ ResourceType.LIST_MEMBERS, ResourceType.LIST_TWEETS, + ResourceType.DM_CONVERSATION, + ResourceType.DM_INBOX_INITIAL_STATE, + ResourceType.DM_INBOX_TIMELINE, ResourceType.TWEET_DETAILS, ResourceType.TWEET_DETAILS_ALT, ResourceType.TWEET_DETAILS_BULK, @@ -54,6 +57,7 @@ export const PostResourcesGroup = [ ResourceType.MEDIA_UPLOAD_APPEND, ResourceType.MEDIA_UPLOAD_FINALIZE, ResourceType.MEDIA_UPLOAD_INITIALIZE, + ResourceType.DM_DELETE_CONVERSATION, ResourceType.TWEET_BOOKMARK, ResourceType.TWEET_LIKE, ResourceType.TWEET_POST, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index e5a21158..9ec21752 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -1,7 +1,7 @@ import { AxiosRequestConfig } from 'axios'; import { ResourceType } from '../enums/Resource'; -import { DMRequests } from '../requests/DM'; +import { DMRequests } from '../requests/DirectMessage'; import { ListRequests } from '../requests/List'; import { MediaRequests } from '../requests/Media'; import { TweetRequests } from '../requests/Tweet'; @@ -29,7 +29,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | DM_CONVERSATION: (args: IFetchArgs) => DMRequests.conversation(args.conversationId!, args.maxId), DM_INBOX_INITIAL_STATE: () => DMRequests.inboxInitial(), DM_INBOX_TIMELINE: (args: IFetchArgs) => DMRequests.inboxTimeline(args.maxId), - DM_USER_UPDATES: (args: IFetchArgs) => DMRequests.userUpdates(args.cursor, args.activeConversationId), + DM_DELETE_CONVERSATION: (args: IPostArgs) => DMRequests.deleteConversation(args.conversationId!), TWEET_BOOKMARK: (args: IPostArgs) => TweetRequests.bookmark(args.id!), TWEET_DETAILS: (args: IFetchArgs) => TweetRequests.details(args.id!), diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index ae3b8754..6d5e8d97 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -17,7 +17,7 @@ export enum ResourceType { DM_CONVERSATION = 'DM_CONVERSATION', DM_INBOX_INITIAL_STATE = 'DM_INBOX_INITIAL_STATE', DM_INBOX_TIMELINE = 'DM_INBOX_TIMELINE', - DM_USER_UPDATES = 'DM_USER_UPDATES', + DM_DELETE_CONVERSATION = 'DM_DELETE_CONVERSATION', // TWEET TWEET_BOOKMARK = 'TWEET_BOOKMARK', diff --git a/src/index.ts b/src/index.ts index 1b5ed2e5..a509c1b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export * from './models/args/PostArgs'; export * from './models/data/Conversation'; export * from './models/data/CursoredData'; export * from './models/data/DirectMessage'; +export * from './models/data/Inbox'; export * from './models/data/List'; export * from './models/data/Notification'; export * from './models/data/Tweet'; @@ -28,7 +29,7 @@ export * from './models/data/User'; export * from './models/errors/TwitterError'; // REQUESTS -export * from './requests/DM'; +export * from './requests/DirectMessage'; export * from './requests/List'; export * from './requests/Media'; export * from './requests/Tweet'; @@ -47,6 +48,7 @@ export * from './types/args/PostArgs'; export * from './types/data/Conversation'; export * from './types/data/CursoredData'; export * from './types/data/DirectMessage'; +export * from './types/data/Inbox'; export * from './types/data/List'; export * from './types/data/Notification'; export * from './types/data/Tweet'; diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index 5350b982..d0f8ac8c 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -8,6 +8,8 @@ import type { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/ra * @public */ export class FetchArgs implements IFetchArgs { + public activeConversationId?: string; + public conversationId?: string; public count?: number; public cursor?: string; public filter?: TweetFilter; @@ -15,6 +17,7 @@ export class FetchArgs implements IFetchArgs { public granularity?: RawAnalyticsGranularity; public id?: string; public ids?: string[]; + public maxId?: string; public metrics?: RawAnalyticsMetric[]; public showVerifiedFollowers?: boolean; public sortBy?: TweetRepliesSortType; @@ -35,6 +38,9 @@ export class FetchArgs implements IFetchArgs { this.granularity = args.granularity; this.metrics = args.metrics; this.showVerifiedFollowers = args.showVerifiedFollowers; + this.activeConversationId = args.activeConversationId; + this.conversationId = args.conversationId; + this.maxId = args.maxId; } } diff --git a/src/models/data/Conversation.ts b/src/models/data/Conversation.ts index d0ed3923..ebe5fca2 100644 --- a/src/models/data/Conversation.ts +++ b/src/models/data/Conversation.ts @@ -1,5 +1,47 @@ import { IConversation } from '../../types/data/Conversation'; -import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { + IInboxInitialResponse, + Conversation as RawConversation, + Conversations as RawConversations, +} from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { DirectMessage } from './DirectMessage'; + +/** + * Type guard to check if the response is an IConversationTimelineResponse + */ +function isConversationTimelineResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IConversationTimelineResponse { + return 'conversation_timeline' in response; +} + +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * Extract typed conversation data from raw conversations object + */ +function extractConversationData(rawConversations: RawConversations): Array<[string, RawConversation]> { + return Object.entries(rawConversations); +} /** * The details of a single conversation. @@ -8,13 +50,14 @@ import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; */ export class Conversation implements IConversation { /** The raw conversation details. */ - private readonly _raw: unknown; + private readonly _raw: RawConversation; public avatarUrl?: string; public hasMore: boolean; public id: string; public lastActivityAt: string; public lastMessageId?: string; + public messages: DirectMessage[]; public muted: boolean; public name?: string; public notificationsDisabled: boolean; @@ -24,66 +67,261 @@ export class Conversation implements IConversation { /** * @param conversation - The raw conversation details from the API response. + * @param messages - Array of messages in this conversation. */ - public constructor(conversation: unknown) { - this._raw = { ...conversation as Record }; + public constructor(conversation: unknown, messages: DirectMessage[] = []) { + this._raw = conversation as RawConversation; const conv = conversation as Record; this.id = conv.conversation_id && typeof conv.conversation_id === 'string' ? conv.conversation_id : ''; - this.type = (conv.type as 'ONE_TO_ONE' | 'GROUP_DM') || 'ONE_TO_ONE'; - - const participants = conv.participants as Array> | undefined; - this.participants = participants?.map((p) => p.user_id && typeof p.user_id === 'string' ? p.user_id : '') || []; - + this.type = this._parseConversationType(conv.type); + this.participants = this._parseParticipants(conv.participants); this.name = conv.name && typeof conv.name === 'string' ? conv.name : undefined; - - const avatar = conv.avatar as Record | undefined; - const image = avatar?.image as Record | undefined; - const originalInfo = image?.original_info as Record | undefined; - this.avatarUrl = (conv.avatar_image_https && typeof conv.avatar_image_https === 'string') - ? conv.avatar_image_https - : (originalInfo?.url && typeof originalInfo.url === 'string') - ? originalInfo.url - : undefined; - + this.avatarUrl = this._parseAvatarUrl(conv); this.trusted = Boolean(conv.trusted); this.muted = Boolean(conv.muted); this.notificationsDisabled = Boolean(conv.notifications_disabled); - this.lastActivityAt = conv.sort_timestamp - ? new Date(Number(conv.sort_timestamp)).toISOString() - : new Date().toISOString(); - this.lastMessageId = conv.sort_event_id && typeof conv.sort_event_id === 'string' ? conv.sort_event_id : undefined; + this.lastActivityAt = this._parseTimestamp(conv.sort_timestamp); + this.lastMessageId = + conv.sort_event_id && typeof conv.sort_event_id === 'string' ? conv.sort_event_id : undefined; this.hasMore = conv.status === 'HAS_MORE'; + this.messages = messages; } /** The raw conversation details. */ - public get raw(): unknown { - return { ...this._raw as Record }; + public get raw(): RawConversation { + return this._raw; + } + + /** + * Parse avatar URL from conversation data + */ + private _parseAvatarUrl(conv: Record): string | undefined { + // Try avatar_image_https first + if (conv.avatar_image_https && typeof conv.avatar_image_https === 'string') { + return conv.avatar_image_https; + } + + // Try nested avatar.image.original_info.url + const avatar = conv.avatar as Record | undefined; + const image = avatar?.image as Record | undefined; + const originalInfo = image?.original_info as Record | undefined; + + if (originalInfo?.url && typeof originalInfo.url === 'string') { + return originalInfo.url; + } + + return undefined; + } + + /** + * Parse conversation type with proper fallback + */ + private _parseConversationType(type: unknown): 'ONE_TO_ONE' | 'GROUP_DM' { + if (type === 'ONE_TO_ONE' || type === 'GROUP_DM') { + return type; + } + return 'ONE_TO_ONE'; + } + + /** + * Parse participants array with type safety + */ + private _parseParticipants(participants: unknown): string[] { + if (!Array.isArray(participants)) { + return []; + } + + return participants + .map((p) => { + if (p && typeof p === 'object' && 'user_id' in p) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const participantObj = p as { user_id: unknown }; + if (typeof participantObj.user_id === 'string') { + return participantObj.user_id; + } + } + return ''; + }) + .filter(Boolean); + } + + /** + * Parse timestamp with proper fallback + */ + private _parseTimestamp(timestamp: unknown): string { + if (timestamp && (typeof timestamp === 'string' || typeof timestamp === 'number')) { + const date = new Date(Number(timestamp)); + if (!isNaN(date.getTime())) { + return date.toISOString(); + } + } + return new Date().toISOString(); + } + + /** + * Extracts a single conversation from conversation timeline response. + * + * @param response - The raw response data. + * + * @returns The deserialized conversation with full message history. + */ + public static fromConversationTimeline(response: IConversationTimelineResponse): Conversation | undefined { + if (!response.conversation_timeline?.conversations) { + return undefined; + } + + const rawConversations = response.conversation_timeline.conversations; + const entries = response.conversation_timeline.entries ?? []; + + // Extract messages from entries + const messages: DirectMessage[] = []; + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + // Get the first (and typically only) conversation + const conversationEntries = extractConversationData(rawConversations); + const firstEntry = conversationEntries[0]; + + if (firstEntry) { + const [, conversationData] = firstEntry; + return new Conversation(conversationData, messages); + } + + return undefined; } /** - * Extracts and deserializes the list of conversations from the given raw response data. + * Extracts conversations from inbox initial state response. * * @param response - The raw response data. * - * @returns The deserialized list of conversations. + * @returns The deserialized list of conversations with their preview messages. */ - public static list(response: NonNullable): Conversation[] { + public static listFromInboxInitial(response: IInboxInitialResponse): Conversation[] { const conversations: Conversation[] = []; - // Handle inbox initial state response - const inboxResponse = response as IInboxInitialResponse; - if (inboxResponse.inbox_initial_state?.conversations) { - const rawConversations = inboxResponse.inbox_initial_state.conversations; - for (const [, conversation] of Object.entries(rawConversations)) { - conversations.push(new Conversation(conversation)); + if (!response.inbox_initial_state?.conversations) { + return conversations; + } + + const rawConversations = response.inbox_initial_state.conversations; + const entries = response.inbox_initial_state.entries ?? []; + + // Group messages by conversation ID + const messagesByConversation = new Map(); + for (const entry of entries) { + if ('message' in entry && entry.message) { + const message = new DirectMessage(entry.message); + const convId = message.conversationId; + if (convId) { + if (!messagesByConversation.has(convId)) { + messagesByConversation.set(convId, []); + } + messagesByConversation.get(convId)!.push(message); + } } } + // Create conversations with their messages + const conversationEntries = extractConversationData(rawConversations); + for (const [, conversation] of conversationEntries) { + const convId = (conversation as unknown as Record).conversation_id as string; + const messages = messagesByConversation.get(convId) ?? []; + conversations.push(new Conversation(conversation, messages)); + } + return conversations; } + /** + * Extracts conversations from inbox timeline response. + * + * @param response - The raw response data. + * + * @returns The deserialized list of conversations with their messages. + */ + public static listFromInboxTimeline(response: IInboxTimelineResponse): Conversation[] { + const conversations: Conversation[] = []; + + if (!response.inbox_timeline?.conversations) { + return conversations; + } + + const rawConversations = response.inbox_timeline.conversations; + const entries = response.inbox_timeline.entries ?? []; + + // Group messages by conversation ID + const messagesByConversation = new Map(); + for (const entry of entries) { + if ('message' in entry && entry.message) { + const message = new DirectMessage(entry.message); + const convId = message.conversationId; + if (convId) { + if (!messagesByConversation.has(convId)) { + messagesByConversation.set(convId, []); + } + messagesByConversation.get(convId)!.push(message); + } + } + } + + // Create conversations with their messages + const conversationEntries = extractConversationData(rawConversations); + for (const [, conversation] of conversationEntries) { + const convId = (conversation as unknown as Record).conversation_id as string; + const messages = messagesByConversation.get(convId) ?? []; + conversations.push(new Conversation(conversation, messages)); + } + + return conversations; + } + + /** + * Generic method to extract conversations from any supported response type + */ + public static listFromResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, + ): Conversation[] { + if (isConversationTimelineResponse(response)) { + const conversation = Conversation.fromConversationTimeline(response); + return conversation ? [conversation] : []; + } else if (isInboxInitialResponse(response)) { + return Conversation.listFromInboxInitial(response); + } else if (isInboxTimelineResponse(response)) { + return Conversation.listFromInboxTimeline(response); + } + return []; + } + + /** + * Get the other participant's ID (only for one-to-one conversations) + */ + public getOtherParticipant(currentUserId: string): string | undefined { + if (!this.isOneToOne() || this.participants.length !== 2) { + return undefined; + } + return this.participants.find((id) => id !== currentUserId); + } + + /** + * Check if this conversation is a group DM + */ + public isGroupDM(): boolean { + return this.type === 'GROUP_DM'; + } + + /** + * Check if this conversation is one-to-one + */ + public isOneToOne(): boolean { + return this.type === 'ONE_TO_ONE'; + } + /** * @returns A serializable JSON representation of `this` object. */ @@ -94,6 +332,7 @@ export class Conversation implements IConversation { id: this.id, lastActivityAt: this.lastActivityAt, lastMessageId: this.lastMessageId, + messages: this.messages.map((msg) => msg.toJSON()), muted: this.muted, name: this.name, notificationsDisabled: this.notificationsDisabled, @@ -102,4 +341,4 @@ export class Conversation implements IConversation { type: this.type, }; } -} \ No newline at end of file +} diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index ebc36290..67aa1e51 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -5,8 +5,6 @@ import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; -import { Conversation } from './Conversation'; -import { DirectMessage } from './DirectMessage'; import { Notification } from './Notification'; import { Tweet } from './Tweet'; import { User } from './User'; @@ -18,7 +16,9 @@ import { User } from './User'; * * @public */ -export class CursoredData implements ICursoredData { +export class CursoredData + implements ICursoredData +{ public list: T[]; public next: string; @@ -31,11 +31,7 @@ export class CursoredData(response, 'cursorType', 'Bottom')[0]?.value ?? ''; } else if (type == BaseType.USER) { @@ -47,39 +43,6 @@ export class CursoredData): string { - const resp = response as Record; - - // Check for inbox_initial_state cursor - const inboxState = resp.inbox_initial_state as Record | undefined; - if (inboxState?.cursor && typeof inboxState.cursor === 'string') { - return inboxState.cursor; - } - - // Check for conversation_timeline min_entry_id for pagination - const conversationTimeline = resp.conversation_timeline as Record | undefined; - if (conversationTimeline?.min_entry_id && typeof conversationTimeline.min_entry_id === 'string') { - return conversationTimeline.min_entry_id; - } - - // Check for inbox_timeline min_entry_id for pagination - const inboxTimeline = resp.inbox_timeline as Record | undefined; - if (inboxTimeline?.min_entry_id && typeof inboxTimeline.min_entry_id === 'string') { - return inboxTimeline.min_entry_id; - } - - // Check for user_events cursor - const userEvents = resp.user_events as Record | undefined; - if (userEvents?.cursor && typeof userEvents.cursor === 'string') { - return userEvents.cursor; - } - - return ''; - } - /** * @returns A serializable JSON representation of `this` object. */ diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts index b47e9bca..a4f28de6 100644 --- a/src/models/data/DirectMessage.ts +++ b/src/models/data/DirectMessage.ts @@ -3,6 +3,73 @@ import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IConversationTimelineResponse + */ +function isConversationTimelineResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IConversationTimelineResponse { + return 'conversation_timeline' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * Interface for parsed message data from various response types + */ +interface IParsedMessageData { + id: string; + conversationId: string; + senderId: string; + recipientId?: string; + text: string; + time: string; + editCount?: number; +} + +/** + * Base interface for raw message data structure + */ +interface IRawMessageBase { + id: string; + time: string; + /* eslint-disable @typescript-eslint/naming-convention */ + conversation_id: string; + message_data?: { + id: string; + time: string; + sender_id: string; + recipient_id?: string; + text: string; + edit_count?: number; + attachment?: unknown; + entities?: unknown; + }; + // Additional properties that may exist in different message types + affects_sort?: boolean; + request_id?: string; + sender_id?: string; + recipient_id?: string; + /* eslint-enable @typescript-eslint/naming-convention */ + text?: string; +} + /** * The details of a single direct message. * @@ -10,7 +77,7 @@ import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; */ export class DirectMessage implements IDirectMessage { /** The raw message details. */ - private readonly _raw: unknown; + private readonly _raw: IRawMessageBase; public conversationId: string; public createdAt: string; @@ -26,68 +93,208 @@ export class DirectMessage implements IDirectMessage { * @param message - The raw message details from the API response. */ public constructor(message: unknown) { - this._raw = { ...message as Record }; + this._raw = message as IRawMessageBase; - const msg = message as Record; - const messageData = msg.message_data as Record | undefined; + const parsedData = this._parseMessageData(message); - this.id = String((messageData?.id && typeof messageData.id === 'string' ? messageData.id : '') || - (msg.id && typeof msg.id === 'string' ? msg.id : '') || ''); - this.conversationId = String((msg.conversation_id && typeof msg.conversation_id === 'string' ? msg.conversation_id : '') || - (messageData?.conversation_id && typeof messageData.conversation_id === 'string' ? messageData.conversation_id : '') || ''); - this.senderId = String((messageData?.sender_id && typeof messageData.sender_id === 'string' ? messageData.sender_id : '') || - (msg.sender_id && typeof msg.sender_id === 'string' ? msg.sender_id : '') || ''); - this.recipientId = (messageData?.recipient_id && typeof messageData.recipient_id === 'string' ? messageData.recipient_id : undefined) || - (msg.recipient_id && typeof msg.recipient_id === 'string' ? msg.recipient_id : undefined); - this.text = String((messageData?.text && typeof messageData.text === 'string' ? messageData.text : '') || - (msg.text && typeof msg.text === 'string' ? msg.text : '') || ''); - this.createdAt = msg.time - ? new Date(Number(msg.time)).toISOString() - : messageData?.time - ? new Date(Number(messageData.time)).toISOString() - : new Date().toISOString(); - this.editCount = Number(messageData?.edit_count) || 0; + this.id = parsedData.id; + this.conversationId = parsedData.conversationId; + this.senderId = parsedData.senderId; + this.recipientId = parsedData.recipientId; + this.text = parsedData.text; + this.createdAt = this._parseTimestamp(parsedData.time); + this.editCount = parsedData.editCount ?? 0; this.mediaUrls = this._extractMediaUrls(message); this.read = true; // Default to true, can be enhanced later } /** The raw message details. */ - public get raw(): unknown { - return { ...this._raw as Record }; + public get raw(): IRawMessageBase { + return this._raw; + } + + /** + * Extract messages from conversation timeline response + */ + private static _extractFromConversationTimeline(response: IConversationTimelineResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.conversation_timeline?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; } /** - * Extract media URLs from message attachment data. + * Extract messages from inbox initial response + */ + private static _extractFromInboxInitial(response: IInboxInitialResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.inbox_initial_state?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; + } + + /** + * Extract messages from inbox timeline response + */ + private static _extractFromInboxTimeline(response: IInboxTimelineResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.inbox_timeline?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; + } + + /** + * Extract media URLs from message attachment data with proper type safety. */ private _extractMediaUrls(message: unknown): string[] | undefined { const urls: string[] = []; const msg = message as Record; const messageData = msg.message_data as Record | undefined; - + // Check for card attachments with images const attachment = messageData?.attachment as Record | undefined; const card = attachment?.card as Record | undefined; const bindingValues = card?.binding_values as Record | undefined; if (bindingValues) { - const thumbnailImage = bindingValues.thumbnail_image as Record | undefined; - const photoImageFullSize = bindingValues.photo_image_full_size as Record | undefined; + // Extract URLs from various image binding values + const imageBindings = [ + 'thumbnail_image', + 'photo_image_full_size', + 'summary_photo_image', + 'thumbnail_image_original', + 'summary_photo_image_original', + 'photo_image_full_size_original', + ]; - if (thumbnailImage?.image_value) { - const imageValue = thumbnailImage.image_value as Record; - if (imageValue.url && typeof imageValue.url === 'string') { + for (const bindingKey of imageBindings) { + const imageBinding = bindingValues[bindingKey] as Record | undefined; + const imageValue = imageBinding?.image_value as Record | undefined; + + if (imageValue?.url && typeof imageValue.url === 'string') { urls.push(imageValue.url); } } - if (photoImageFullSize?.image_value) { - const imageValue = photoImageFullSize.image_value as Record; - if (imageValue.url && typeof imageValue.url === 'string') { - urls.push(imageValue.url); - } + } + + // Check for tweet attachments + const tweet = attachment?.tweet as Record | undefined; + if (tweet?.expanded_url && typeof tweet.expanded_url === 'string') { + urls.push(tweet.expanded_url); + } + + return urls.length > 0 ? [...new Set(urls)] : undefined; // Remove duplicates + } + + /** + * Safely extract number value + */ + private _extractNumberValue(value: unknown): number | undefined { + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string') { + const parsed = Number(value); + return isNaN(parsed) ? undefined : parsed; + } + return undefined; + } + + /** + * Safely extract string value with fallback + */ + private _extractStringValue(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === 'string' && value.length > 0) { + return value; + } + } + return undefined; + } + + /** + * Parse message data with proper type safety + */ + private _parseMessageData(message: unknown): IParsedMessageData { + const msg = message as Record; + const messageData = msg.message_data as Record | undefined; + + // Extract ID + const id = this._extractStringValue(messageData?.id, msg.id) ?? ''; + + // Extract conversation ID + const conversationId = this._extractStringValue(msg.conversation_id, messageData?.conversation_id) ?? ''; + + // Extract sender ID + const senderId = this._extractStringValue(messageData?.sender_id, msg.sender_id) ?? ''; + + // Extract recipient ID (optional) + const recipientId = this._extractStringValue(messageData?.recipient_id, msg.recipient_id); + + // Extract text + const text = this._extractStringValue(messageData?.text, msg.text) ?? ''; + + // Extract time + const time = this._extractStringValue(msg.time, messageData?.time) ?? new Date().getTime().toString(); + + // Extract edit count + const editCount = this._extractNumberValue(messageData?.edit_count); + + return { + id, + conversationId, + senderId, + recipientId, + text, + time, + editCount, + }; + } + + /** + * Parse timestamp with proper validation + */ + private _parseTimestamp(timestamp: string): string { + const numericTimestamp = Number(timestamp); + if (!isNaN(numericTimestamp)) { + const date = new Date(numericTimestamp); + if (!isNaN(date.getTime())) { + return date.toISOString(); } } + return new Date().toISOString(); + } + + /** + * Filter messages by conversation ID + */ + public static filterByConversation(messages: DirectMessage[], conversationId: string): DirectMessage[] { + return messages.filter((message) => message.conversationId === conversationId); + } - return urls.length > 0 ? urls : undefined; + /** + * Filter messages by sender ID + */ + public static filterBySender(messages: DirectMessage[], senderId: string): DirectMessage[] { + return messages.filter((message) => message.isFromSender(senderId)); } /** @@ -97,43 +304,61 @@ export class DirectMessage implements IDirectMessage { * * @returns The deserialized list of direct messages. */ - public static list(response: NonNullable): DirectMessage[] { + public static list( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, + ): DirectMessage[] { const messages: DirectMessage[] = []; - // Handle inbox initial state response - const inboxResponse = response as IInboxInitialResponse; - if (inboxResponse.inbox_initial_state?.entries) { - const entries = inboxResponse.inbox_initial_state.entries; - for (const entry of entries) { - if ('message' in entry && entry.message) { - messages.push(new DirectMessage(entry.message)); - } - } + if (isInboxInitialResponse(response)) { + return DirectMessage._extractFromInboxInitial(response); + } else if (isConversationTimelineResponse(response)) { + return DirectMessage._extractFromConversationTimeline(response); + } else if (isInboxTimelineResponse(response)) { + return DirectMessage._extractFromInboxTimeline(response); } - // Handle conversation timeline response - const conversationResponse = response as IConversationTimelineResponse; - if (conversationResponse.conversation_timeline?.entries) { - const entries = conversationResponse.conversation_timeline.entries; - for (const entry of entries) { - if ('message' in entry && entry.message) { - messages.push(new DirectMessage(entry.message)); - } - } - } + return messages; + } - // Handle inbox timeline response - const timelineResponse = response as IInboxTimelineResponse; - if (timelineResponse.inbox_timeline?.entries) { - const entries = timelineResponse.inbox_timeline.entries; - for (const entry of entries) { - if ('message' in entry && entry.message) { - messages.push(new DirectMessage(entry.message)); - } - } - } + /** + * Generic method to extract messages from any supported response type + */ + public static listFromResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, + ): DirectMessage[] { + return DirectMessage.list(response); + } - return messages; + /** + * Sort messages by creation time (oldest to newest) + */ + public static sortByTime(messages: DirectMessage[], ascending = true): DirectMessage[] { + return [...messages].sort((a, b) => { + const timeA = new Date(a.createdAt).getTime(); + const timeB = new Date(b.createdAt).getTime(); + return ascending ? timeA - timeB : timeB - timeA; + }); + } + + /** + * Get the age of this message in milliseconds + */ + public getAgeInMs(): number { + return Date.now() - new Date(this.createdAt).getTime(); + } + + /** + * Check if this message has media attachments + */ + public hasMedia(): boolean { + return Boolean(this.mediaUrls && this.mediaUrls.length > 0); + } + + /** + * Check if this message is from a specific sender + */ + public isFromSender(senderId: string): boolean { + return this.senderId === senderId; } /** @@ -152,4 +377,11 @@ export class DirectMessage implements IDirectMessage { text: this.text, }; } -} \ No newline at end of file + + /** + * Check if this message was edited + */ + public wasEdited(): boolean { + return Boolean(this.editCount && this.editCount > 0); + } +} diff --git a/src/models/data/Inbox.ts b/src/models/data/Inbox.ts new file mode 100644 index 00000000..1a1a7cf2 --- /dev/null +++ b/src/models/data/Inbox.ts @@ -0,0 +1,124 @@ +import { IInbox } from '../../types/data/Inbox'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { Conversation } from './Conversation'; + +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * The details of a DM inbox containing conversations and metadata. + * + * @public + */ +export class Inbox implements IInbox { + /** The raw inbox details. */ + private readonly _raw: IInboxInitialResponse | IInboxTimelineResponse; + + public conversations: Conversation[]; + public cursor: string; + public lastSeenEventId: string; + public trustedLastSeenEventId: string; + public untrustedLastSeenEventId: string; + + /** + * @param response - The raw inbox response from the API. + */ + public constructor(response: IInboxInitialResponse | IInboxTimelineResponse) { + this._raw = response; + + // Handle inbox initial state response + if (isInboxInitialResponse(response)) { + const inboxState = response.inbox_initial_state; + + this.cursor = inboxState.cursor ?? ''; + this.lastSeenEventId = inboxState.last_seen_event_id ?? ''; + this.trustedLastSeenEventId = inboxState.trusted_last_seen_event_id ?? ''; + this.untrustedLastSeenEventId = inboxState.untrusted_last_seen_event_id ?? ''; + + // Parse conversations from inbox initial state + this.conversations = Conversation.listFromInboxInitial(response); + } + // Handle inbox timeline response + else if (isInboxTimelineResponse(response)) { + const inboxTimeline = response.inbox_timeline; + + this.cursor = inboxTimeline.min_entry_id ?? ''; + this.lastSeenEventId = ''; + this.trustedLastSeenEventId = ''; + this.untrustedLastSeenEventId = ''; + + // Parse conversations from inbox timeline + this.conversations = Conversation.listFromInboxTimeline(response); + } else { + // Fallback defaults (this should never happen with proper typing) + this.cursor = ''; + this.lastSeenEventId = ''; + this.trustedLastSeenEventId = ''; + this.untrustedLastSeenEventId = ''; + this.conversations = []; + } + } + + /** The raw inbox details. */ + public get raw(): IInboxInitialResponse | IInboxTimelineResponse { + return this._raw; + } + + /** + * Get the raw inbox initial state if this inbox was created from one + */ + public getInitialState(): IInboxInitialResponse | undefined { + return this.isInitialState() ? (this._raw as IInboxInitialResponse) : undefined; + } + + /** + * Get the raw inbox timeline if this inbox was created from one + */ + public getTimeline(): IInboxTimelineResponse | undefined { + return this.isTimeline() ? (this._raw as IInboxTimelineResponse) : undefined; + } + + /** + * Check if this inbox was created from an initial state response + */ + public isInitialState(): boolean { + return isInboxInitialResponse(this._raw); + } + + /** + * Check if this inbox was created from a timeline response + */ + public isTimeline(): boolean { + return isInboxTimelineResponse(this._raw); + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IInbox { + return { + conversations: this.conversations.map((conv) => conv.toJSON()), + cursor: this.cursor, + lastSeenEventId: this.lastSeenEventId, + trustedLastSeenEventId: this.trustedLastSeenEventId, + untrustedLastSeenEventId: this.untrustedLastSeenEventId, + }; + } +} diff --git a/src/requests/DM.ts b/src/requests/DirectMessage.ts similarity index 64% rename from src/requests/DM.ts rename to src/requests/DirectMessage.ts index 1cc7b08e..ab3fbe0c 100644 --- a/src/requests/DM.ts +++ b/src/requests/DirectMessage.ts @@ -81,6 +81,37 @@ export class DMRequests { }; } + /** + * Delete a DM conversation + * @param conversationId - The ID of the conversation to delete + */ + public static deleteConversation(conversationId: string): AxiosRequestConfig { + return { + method: 'post', + url: `https://x.com/i/api/1.1/dm/${conversationId}/delete.json`, + data: qs.stringify({ + /* eslint-disable @typescript-eslint/naming-convention */ + dm_secret_conversations_enabled: false, + krs_registration_enabled: false, + cards_platform: 'Web-12', + include_cards: 1, + include_ext_alt_text: true, + include_ext_limited_action_results: true, + include_quote_count: true, + include_reply_count: 1, + tweet_mode: 'extended', + include_ext_views: true, + dm_users: false, + include_groups: true, + include_inbox_timelines: true, + include_ext_media_color: true, + supports_reactions: true, + supports_edit: true, + include_conversation_info: true, + }), + }; + } + /** * Get the initial state of the DM inbox */ @@ -122,79 +153,79 @@ export class DMRequests { /** * Create a new DM or get DM creation interface */ - public static new(): AxiosRequestConfig { - return { - method: 'get', - url: 'https://x.com/i/api/1.1/dm/new2.json', - params: { - /* eslint-disable @typescript-eslint/naming-convention */ - ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', - include_ext_alt_text: true, - include_ext_limited_action_results: true, - include_reply_count: 1, - tweet_mode: 'extended', - include_ext_views: true, - include_groups: true, - include_inbox_timelines: true, - include_ext_media_color: true, - supports_reactions: true, - supports_edit: true, - }, - paramsSerializer: { encode: encodeURIComponent }, - }; - } + // public static new(): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/new2.json', + // params: { + // /* eslint-disable @typescript-eslint/naming-convention */ + // ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + // include_ext_alt_text: true, + // include_ext_limited_action_results: true, + // include_reply_count: 1, + // tweet_mode: 'extended', + // include_ext_views: true, + // include_groups: true, + // include_inbox_timelines: true, + // include_ext_media_color: true, + // supports_reactions: true, + // supports_edit: true, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } /** * Check DM permissions for specific recipients * @param recipientIds - Array of recipient user IDs */ - public static permissions(recipientIds: string[]): AxiosRequestConfig { - return { - method: 'get', - url: 'https://x.com/i/api/1.1/dm/permissions.json', - params: { - /* eslint-disable @typescript-eslint/naming-convention */ - recipient_ids: recipientIds.join(','), - dm_users: true, - }, - paramsSerializer: { encode: encodeURIComponent }, - }; - } + // public static permissions(recipientIds: string[]): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/permissions.json', + // params: { + // /* eslint-disable @typescript-eslint/naming-convention */ + // recipient_ids: recipientIds.join(','), + // dm_users: true, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } /** * Update the last seen event ID for a conversation * @param lastSeenEventId - The ID of the last seen event * @param trustedLastSeenEventId - The trusted last seen event ID (usually same as lastSeenEventId) */ - public static updateLastSeenEventId(lastSeenEventId: string, trustedLastSeenEventId?: string): AxiosRequestConfig { - return { - method: 'post', - url: 'https://x.com/i/api/1.1/dm/update_last_seen_event_id.json', - data: qs.stringify({ - /* eslint-disable @typescript-eslint/naming-convention */ - last_seen_event_id: lastSeenEventId, - trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId, - }), - }; - } + // public static updateLastSeenEventId(lastSeenEventId: string, trustedLastSeenEventId?: string): AxiosRequestConfig { + // return { + // method: 'post', + // url: 'https://x.com/i/api/1.1/dm/update_last_seen_event_id.json', + // data: qs.stringify({ + // /* eslint-disable @typescript-eslint/naming-convention */ + // last_seen_event_id: lastSeenEventId, + // trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId, + // }), + // }; + // } /** * Get user updates for DMs (polling for new messages) * @param cursor - Cursor for pagination * @param activeConversationId - ID of the currently active conversation */ - public static userUpdates(cursor?: string, activeConversationId?: string): AxiosRequestConfig { - return { - method: 'get', - url: 'https://x.com/i/api/1.1/dm/user_updates.json', - params: { - ...DM_BASE_PARAMS, - /* eslint-disable @typescript-eslint/naming-convention */ - cursor: cursor, - active_conversation_id: activeConversationId, - dm_users: false, - }, - paramsSerializer: { encode: encodeURIComponent }, - }; - } + // public static userUpdates(cursor?: string, activeConversationId?: string): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/user_updates.json', + // params: { + // ...DM_BASE_PARAMS, + // /* eslint-disable @typescript-eslint/naming-convention */ + // cursor: cursor, + // active_conversation_id: activeConversationId, + // dm_users: false, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } } diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index e0e3e2d3..5a19e1ee 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -1,7 +1,7 @@ import { Extractors } from '../../collections/Extractors'; import { ResourceType } from '../../enums/Resource'; -import { CursoredData } from '../../models/data/CursoredData'; -import { DirectMessage } from '../../models/data/DirectMessage'; +import { Conversation } from '../../models/data/Conversation'; +import { Inbox } from '../../models/data/Inbox'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; @@ -24,14 +24,14 @@ export class DirectMessageService extends FetcherService { super(config); } - /** + /** * Get the full conversation history for a specific conversation. * Use this to load complete message history for a conversation identified from the inbox. * * @param conversationId - The ID of the conversation (e.g., "394028042-1712730991884689408"). * @param cursor - The cursor for pagination (maxId from previous response). * - * @returns The conversation timeline with messages. + * @returns The conversation with full message history, or undefined if not found. * * @example * @@ -42,21 +42,24 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching a specific conversation - * rettiwt.dm.conversation('394028042-1712730991884689408') - * .then(res => { - * console.log(res); + * rettiwt.directMessage.conversation('394028042-1712730991884689408') + * .then(conversation => { + * if (conversation) { + * console.log(`Conversation with ${conversation.participants.length} participants`); + * console.log(`${conversation.messages.length} messages loaded`); + * } * }) * .catch(err => { * console.log(err); * }); * ``` */ - public async conversation(conversationId: string, cursor?: string): Promise> { + public async conversation(conversationId: string, cursor?: string): Promise { const resource = ResourceType.DM_CONVERSATION; // Fetching raw conversation timeline const response = await this.request(resource, { - conversationId: conversationId, + conversationId, maxId: cursor, }); @@ -66,11 +69,44 @@ export class DirectMessageService extends FetcherService { return data; } + /** + * Delete a conversation. + * You will leave the conversation and it will be removed from your inbox. + * + * @param conversationId - The ID of the conversation to delete. + * + * @returns A promise that resolves when the conversation is deleted. + * + * @example + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * // Deleting a conversation + * rettiwt.directMessage.deleteConversation('394028042-1712730991884689408') + * .then(() => { + * console.log('Conversation deleted successfully'); + * }) + * .catch(err => { + * console.log('Failed to delete conversation:', err); + * }); + * ``` + **/ + public async deleteConversation(conversationId: string): Promise { + const resource = ResourceType.DM_DELETE_CONVERSATION; + + // Sending delete request + await this.request(resource, { + conversationId, + }); + } + /** * Get the initial state of the DM inbox, including recent conversations and messages. * This is the main entry point for the DM system following the "Inbox as Entry Point" pattern. * - * @returns The initial DM inbox state with recent messages and conversations. + * @returns The initial DM inbox state with conversations containing preview messages. * * @example * @@ -81,16 +117,17 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching the initial DM inbox state - * rettiwt.dm.inbox() - * .then(res => { - * console.log(res); + * rettiwt.directMessage.inbox() + * .then(inbox => { + * console.log(`Found ${inbox.conversations.length} conversations`); + * console.log('First conversation:', inbox.conversations[0]); * }) * .catch(err => { * console.log(err); * }); * ``` */ - public async inbox(): Promise> { + public async inbox(): Promise { const resource = ResourceType.DM_INBOX_INITIAL_STATE; // Fetching raw inbox initial state @@ -102,8 +139,6 @@ export class DirectMessageService extends FetcherService { return data; } - - /** * Get more conversations from the inbox timeline (for pagination). * Use this to load older conversations beyond what's included in the initial inbox state. @@ -121,16 +156,16 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching older conversations using pagination - * rettiwt.dm.inboxTimeline('1803853649426133349') - * .then(res => { - * console.log(res); + * rettiwt.directMessage.inboxTimeline('1803853649426133349') + * .then(inbox => { + * console.log(`Found ${inbox.conversations.length} additional conversations`); * }) * .catch(err => { * console.log(err); * }); * ``` */ - public async inboxTimeline(cursor?: string): Promise> { + public async inboxTimeline(cursor?: string): Promise { const resource = ResourceType.DM_INBOX_TIMELINE; // Fetching raw inbox timeline @@ -143,81 +178,4 @@ export class DirectMessageService extends FetcherService { return data; } - - - - /** - * Stream new DM updates in pseudo real-time by polling the user updates endpoint. - * This can be used to detect new messages and conversation changes. - * - * @param pollingInterval - The interval in milliseconds to poll for new updates. Default interval is 30000 ms (30 seconds). - * @param activeConversationId - ID of the currently active conversation for context-aware updates. - * - * @returns An async generator that yields new messages as they are received. - * - * @example - * - * ```ts - * import { Rettiwt } from 'rettiwt-api'; - * - * // Creating a new Rettiwt instance using the given 'API_KEY' - * const rettiwt = new Rettiwt({ apiKey: API_KEY }); - * - * // Creating a function that streams all new DM updates - * async function streamDMUpdates() { - * try { - * // Awaiting for the messages returned by the AsyncGenerator - * for await (const message of rettiwt.dm.streamUpdates(10000)) { - * console.log(`New message: ${message.text}`); - * } - * } - * catch (err) { - * console.log(err); - * } - * } - * - * // Calling the function - * streamDMUpdates(); - * ``` - */ - public async *streamUpdates(pollingInterval = 30000, activeConversationId?: string): AsyncGenerator { - const resource = ResourceType.DM_USER_UPDATES; - - /** Whether it's the first batch of updates or not. */ - let first = true; - - /** The cursor to the last update received. */ - let cursor: string | undefined = undefined; - - while (true) { - // Pause execution for the specified polling interval before proceeding to the next iteration - await new Promise((resolve) => setTimeout(resolve, pollingInterval)); - - // Get the batch of updates after the given cursor - const response = await this.request(resource, { - cursor: cursor, - activeConversationId: activeConversationId, - }); - - // Deserializing response - const updates = Extractors[resource](response); - - // Sorting the messages by time, from oldest to recent - updates.list.sort((a, b) => new Date(a.createdAt).valueOf() - new Date(b.createdAt).valueOf()); - - // If not first batch, return new messages - if (!first) { - // Yield the new messages - for (const message of updates.list) { - yield message; - } - } - // Else do nothing since first batch contains messages that have already been received - else { - first = false; - } - - cursor = updates.next; - } - } -} \ No newline at end of file +} diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index 225f7481..d21dd683 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -7,10 +7,9 @@ import { TweetRepliesSortType } from '../../enums/Tweet'; * @public */ export interface IFetchArgs { - /** * The id of the active conversation. - * + * * @remarks * - Required only for {@link ResourceType.DM_USER_UPDATES}. */ @@ -28,7 +27,7 @@ export interface IFetchArgs { * The id of the conversation to fetch. * * @remarks - * - Required only for {@link ResourceType.DM_CONVERSATION}. + * - Required only for {@link ResourceType.DM_CONVERSATION} and {@link ResourceType.DM_DELETE_CONVERSATION}. */ conversationId?: string; @@ -50,8 +49,6 @@ export interface IFetchArgs { */ count?: number; - - /** * The cursor to the batch of data to fetch. * diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index 4930f5f4..4ef66143 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -39,6 +39,14 @@ export interface IPostArgs { * - {@link ResourceType.MEDIA_UPLOAD_INITIALIZE} */ upload?: IUploadArgs; + + /** + * The id of the conversation to delete. + * + * @remarks + * Required only when deleting a conversation using {@link ResourceType.DM_DELETE_CONVERSATION} + */ + conversationId?: string; } /** diff --git a/src/types/data/Conversation.ts b/src/types/data/Conversation.ts index 702f3e2a..2a4ba811 100644 --- a/src/types/data/Conversation.ts +++ b/src/types/data/Conversation.ts @@ -1,3 +1,5 @@ +import { IDirectMessage } from './DirectMessage'; + /** * The details of a single conversation. * @@ -36,4 +38,7 @@ export interface IConversation { /** Whether there are more messages to load. */ hasMore: boolean; -} \ No newline at end of file + + /** Array of messages in this conversation. */ + messages: IDirectMessage[]; +} diff --git a/src/types/data/DirectMessage.ts b/src/types/data/DirectMessage.ts index 29826c98..1212c922 100644 --- a/src/types/data/DirectMessage.ts +++ b/src/types/data/DirectMessage.ts @@ -30,4 +30,4 @@ export interface IDirectMessage { /** Whether the message has been read. */ read?: boolean; -} \ No newline at end of file +} diff --git a/src/types/data/Inbox.ts b/src/types/data/Inbox.ts new file mode 100644 index 00000000..bf867d0a --- /dev/null +++ b/src/types/data/Inbox.ts @@ -0,0 +1,23 @@ +import { IConversation } from './Conversation'; + +/** + * The details of a DM inbox containing conversations and metadata. + * + * @public + */ +export interface IInbox { + /** List of conversations in the inbox. */ + conversations: IConversation[]; + + /** The cursor for pagination of conversations. */ + cursor: string; + + /** The ID of the last seen event. */ + lastSeenEventId: string; + + /** The ID of the last seen trusted event. */ + trustedLastSeenEventId: string; + + /** The ID of the last seen untrusted event. */ + untrustedLastSeenEventId: string; +} diff --git a/src/types/raw/dm/InboxInitial.ts b/src/types/raw/dm/InboxInitial.ts index 0d9ae7c3..2faa5859 100644 --- a/src/types/raw/dm/InboxInitial.ts +++ b/src/types/raw/dm/InboxInitial.ts @@ -105,7 +105,7 @@ export interface Conversations { [conversationId: string]: Conversation; } -interface Conversation { +export interface Conversation { conversation_id: string; type: 'GROUP_DM' | 'ONE_TO_ONE'; sort_event_id: string; @@ -148,7 +148,7 @@ interface Participant { is_admin?: boolean; // Only for GROUP_DM } -interface SocialProof { +export interface SocialProof { proof_type: string; // e.g., "mutual_friends" users: any[]; // Array of users (structure depends on proof_type) total: number; From 8d26451b17f425f16131c40f88ad0e64ee0f54db Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 18 Jul 2025 19:11:51 +0200 Subject: [PATCH 053/119] refactor: rename directMessage to dm --- src/Rettiwt.ts | 4 ++-- src/services/public/DirectMessageService.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Rettiwt.ts b/src/Rettiwt.ts index 1be5c6a5..96ccdccc 100644 --- a/src/Rettiwt.ts +++ b/src/Rettiwt.ts @@ -51,7 +51,7 @@ export class Rettiwt { private _config: RettiwtConfig; /** The instance used to fetch data related to direct messages. */ - public directMessage: DirectMessageService; + public dm: DirectMessageService; /** The instance used to fetch data related to lists. */ public list: ListService; @@ -69,7 +69,7 @@ export class Rettiwt { */ public constructor(config?: IRettiwtConfig) { this._config = new RettiwtConfig(config); - this.directMessage = new DirectMessageService(this._config); + this.dm = new DirectMessageService(this._config); this.list = new ListService(this._config); this.tweet = new TweetService(this._config); this.user = new UserService(this._config); diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index 5a19e1ee..704a8b13 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -42,7 +42,7 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching a specific conversation - * rettiwt.directMessage.conversation('394028042-1712730991884689408') + * rettiwt.dm.conversation('394028042-1712730991884689408') * .then(conversation => { * if (conversation) { * console.log(`Conversation with ${conversation.participants.length} participants`); @@ -84,7 +84,7 @@ export class DirectMessageService extends FetcherService { * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * // Deleting a conversation - * rettiwt.directMessage.deleteConversation('394028042-1712730991884689408') + * rettiwt.dm.deleteConversation('394028042-1712730991884689408') * .then(() => { * console.log('Conversation deleted successfully'); * }) @@ -117,7 +117,7 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching the initial DM inbox state - * rettiwt.directMessage.inbox() + * rettiwt.dm.inbox() * .then(inbox => { * console.log(`Found ${inbox.conversations.length} conversations`); * console.log('First conversation:', inbox.conversations[0]); @@ -156,7 +156,7 @@ export class DirectMessageService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching older conversations using pagination - * rettiwt.directMessage.inboxTimeline('1803853649426133349') + * rettiwt.dm.inboxTimeline('1803853649426133349') * .then(inbox => { * console.log(`Found ${inbox.conversations.length} additional conversations`); * }) From 05ef064972372f3a8a78b6262b82998e0fa40bfe Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 18 Jul 2025 19:19:54 +0200 Subject: [PATCH 054/119] refactor: dm model data --- src/models/data/DirectMessage.ts | 34 ++++---------------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts index a4f28de6..840db53e 100644 --- a/src/models/data/DirectMessage.ts +++ b/src/models/data/DirectMessage.ts @@ -30,19 +30,6 @@ function isInboxTimelineResponse( return 'inbox_timeline' in response; } -/** - * Interface for parsed message data from various response types - */ -interface IParsedMessageData { - id: string; - conversationId: string; - senderId: string; - recipientId?: string; - text: string; - time: string; - editCount?: number; -} - /** * Base interface for raw message data structure */ @@ -102,7 +89,7 @@ export class DirectMessage implements IDirectMessage { this.senderId = parsedData.senderId; this.recipientId = parsedData.recipientId; this.text = parsedData.text; - this.createdAt = this._parseTimestamp(parsedData.time); + this.createdAt = parsedData.createdAt; this.editCount = parsedData.editCount ?? 0; this.mediaUrls = this._extractMediaUrls(message); this.read = true; // Default to true, can be enhanced later @@ -233,29 +220,16 @@ export class DirectMessage implements IDirectMessage { /** * Parse message data with proper type safety */ - private _parseMessageData(message: unknown): IParsedMessageData { + private _parseMessageData(message: unknown): IDirectMessage { const msg = message as Record; const messageData = msg.message_data as Record | undefined; - // Extract ID const id = this._extractStringValue(messageData?.id, msg.id) ?? ''; - - // Extract conversation ID const conversationId = this._extractStringValue(msg.conversation_id, messageData?.conversation_id) ?? ''; - - // Extract sender ID const senderId = this._extractStringValue(messageData?.sender_id, msg.sender_id) ?? ''; - - // Extract recipient ID (optional) const recipientId = this._extractStringValue(messageData?.recipient_id, msg.recipient_id); - - // Extract text const text = this._extractStringValue(messageData?.text, msg.text) ?? ''; - - // Extract time - const time = this._extractStringValue(msg.time, messageData?.time) ?? new Date().getTime().toString(); - - // Extract edit count + const createdAt = this._parseTimestamp(this._extractStringValue(messageData?.time, msg.time) ?? ''); const editCount = this._extractNumberValue(messageData?.edit_count); return { @@ -263,8 +237,8 @@ export class DirectMessage implements IDirectMessage { conversationId, senderId, recipientId, + createdAt, text, - time, editCount, }; } From ece87e4a27aa6ed004e929e3772bf2cc9b878e5f Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 18 Jul 2025 19:38:34 +0200 Subject: [PATCH 055/119] refactor: types DirectMessage model --- src/models/data/DirectMessage.ts | 30 ++---------------------------- src/types/raw/dm/Conversation.ts | 2 +- src/types/raw/dm/InboxInitial.ts | 2 +- src/types/raw/dm/InboxTimeline.ts | 2 +- src/types/raw/dm/Message.ts | 22 ++++++++++++++++++++++ 5 files changed, 27 insertions(+), 31 deletions(-) create mode 100644 src/types/raw/dm/Message.ts diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts index 840db53e..5dc46c21 100644 --- a/src/models/data/DirectMessage.ts +++ b/src/models/data/DirectMessage.ts @@ -1,7 +1,8 @@ import { IDirectMessage } from '../../types/data/DirectMessage'; -import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { IConversationTimelineResponse} from '../../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; +import { IRawMessageBase } from '../../types/raw/dm/Message'; /** * Type guard to check if the response is an IInboxInitialResponse @@ -30,33 +31,6 @@ function isInboxTimelineResponse( return 'inbox_timeline' in response; } -/** - * Base interface for raw message data structure - */ -interface IRawMessageBase { - id: string; - time: string; - /* eslint-disable @typescript-eslint/naming-convention */ - conversation_id: string; - message_data?: { - id: string; - time: string; - sender_id: string; - recipient_id?: string; - text: string; - edit_count?: number; - attachment?: unknown; - entities?: unknown; - }; - // Additional properties that may exist in different message types - affects_sort?: boolean; - request_id?: string; - sender_id?: string; - recipient_id?: string; - /* eslint-enable @typescript-eslint/naming-convention */ - text?: string; -} - /** * The details of a single direct message. * diff --git a/src/types/raw/dm/Conversation.ts b/src/types/raw/dm/Conversation.ts index ef9aeb6b..cc9084e4 100644 --- a/src/types/raw/dm/Conversation.ts +++ b/src/types/raw/dm/Conversation.ts @@ -22,7 +22,7 @@ interface ConversationTimeline { type ConversationEntry = { message: ConversationMessage } | { trust_conversation: TrustConversation }; -interface ConversationMessage { +export interface ConversationMessage { id: string; time: string; request_id: string; diff --git a/src/types/raw/dm/InboxInitial.ts b/src/types/raw/dm/InboxInitial.ts index 2faa5859..419ccf44 100644 --- a/src/types/raw/dm/InboxInitial.ts +++ b/src/types/raw/dm/InboxInitial.ts @@ -35,7 +35,7 @@ interface Entry { message: Message; } -interface Message { +export interface Message { id: string; time: string; affects_sort: boolean; diff --git a/src/types/raw/dm/InboxTimeline.ts b/src/types/raw/dm/InboxTimeline.ts index a4bfa550..d6478ace 100644 --- a/src/types/raw/dm/InboxTimeline.ts +++ b/src/types/raw/dm/InboxTimeline.ts @@ -33,7 +33,7 @@ interface TrustConversation { reason: string; // e.g., "accept" } -interface TimelineMessage { +export interface TimelineMessage { id: string; time: string; affects_sort: boolean; diff --git a/src/types/raw/dm/Message.ts b/src/types/raw/dm/Message.ts new file mode 100644 index 00000000..bd3a5cf8 --- /dev/null +++ b/src/types/raw/dm/Message.ts @@ -0,0 +1,22 @@ +import { ConversationMessage } from './Conversation'; +import { Message } from './InboxInitial'; +import { TimelineMessage } from './InboxTimeline'; + +// Extract the message_data types +type ConversationMessageData = ConversationMessage['message_data']; +type InboxMessageData = Message['message_data']; +type TimelineMessageData = TimelineMessage['message_data']; + +// Create unified message_data type that includes all possible fields +type UnifiedMessageData = InboxMessageData & Partial & Partial; + +/* eslint-disable @typescript-eslint/naming-convention */ +export interface IRawMessageBase { + id: string; + time: string; + affects_sort?: boolean; + request_id: string; + conversation_id: string; + message_data: UnifiedMessageData; +} +/* eslint-enable @typescript-eslint/naming-convention */ From e2ce023504797f9e52942d5573a4561389ceffcd Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 18 Jul 2025 19:43:42 +0200 Subject: [PATCH 056/119] docs: update README --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a67397a..61d009b1 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - 'User' authentication (logging in) grants access to the following resources/actions: + - Direct Message Inbox + - Direct Message Conversations + - Direct Message Delete Conversation - List Add Member - List Members - List Remove Member @@ -129,8 +132,9 @@ A new Rettiwt instance can be initialized using the following code snippets: - `const rettiwt = new Rettiwt()` (for 'guest' authentication) - `const rettiwt = new Rettiwt({ apiKey: API_KEY })` (for 'user' authentication) -The Rettiwt class has three members: +The Rettiwt class has four members: +- `dm` member, for accessing resources related to direct messages. - `list` memeber, for accessing resources related to lists. - `tweet` member, for accessing resources related to tweets. - `user` member, for accessing resources related to users. @@ -439,6 +443,13 @@ For handling and processing of data returned by the functions, it's always advis So far, the following operations are supported: +### Direct Messages + +- [Getting the initial DM inbox state](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inbox) +- [Getting a specific conversation with full message history](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#conversation) +- [Getting more conversations from inbox timeline (pagination)](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inboxTimeline) +- [Deleting a conversation](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#deleteConversation) + ### List - [Adding a member to a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#addMember) From 688280ea36d9d16bb24efe61c80d808c228d8ddd Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 18 Jul 2025 19:49:09 +0200 Subject: [PATCH 057/119] feat: add CLI dm support --- src/cli.ts | 2 + src/commands/DirectMessage.ts | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/commands/DirectMessage.ts diff --git a/src/cli.ts b/src/cli.ts index 83f46818..a096cbaf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { createCommand } from 'commander'; +import dm from './commands/DirectMessage'; import list from './commands/List'; import tweet from './commands/Tweet'; import user from './commands/User'; @@ -38,6 +39,7 @@ const RettiwtInstance = new Rettiwt({ }); // Adding sub-commands +Program.addCommand(dm(RettiwtInstance)); Program.addCommand(list(RettiwtInstance)); Program.addCommand(tweet(RettiwtInstance)); Program.addCommand(user(RettiwtInstance)); diff --git a/src/commands/DirectMessage.ts b/src/commands/DirectMessage.ts new file mode 100644 index 00000000..9fe1b069 --- /dev/null +++ b/src/commands/DirectMessage.ts @@ -0,0 +1,71 @@ +import { Command, createCommand } from 'commander'; + +import { output } from '../helper/CliUtils'; +import { Rettiwt } from '../Rettiwt'; + +/** + * Creates a new 'dm' command which uses the given Rettiwt instance. + * + * @param rettiwt - The Rettiwt instance to use. + * @returns The created 'dm' command. + */ +function createDirectMessageCommand(rettiwt: Rettiwt): Command { + // Creating the 'dm' command + const dm = createCommand('dm').description('Access resources related to direct messages'); + + // Conversation + dm.command('conversation') + .description('Get the full conversation history for a specific conversation') + .argument('', 'The ID of the conversation (e.g., "394028042-1712730991884689408")') + .argument('[cursor]', 'The cursor for pagination (maxId from previous response)') + .action(async (conversationId: string, cursor?: string) => { + try { + const conversation = await rettiwt.dm.conversation(conversationId, cursor); + output(conversation); + } catch (error) { + output(error); + } + }); + + // Delete conversation + dm.command('delete-conversation') + .description('Delete a conversation (you will leave the conversation and it will be removed from your inbox)') + .argument('', 'The ID of the conversation to delete') + .action(async (conversationId: string) => { + try { + await rettiwt.dm.deleteConversation(conversationId); + output({ success: true, message: 'Conversation deleted successfully' }); + } catch (error) { + output(error); + } + }); + + // Inbox + dm.command('inbox') + .description('Get the initial state of the DM inbox, including recent conversations and messages') + .action(async () => { + try { + const inbox = await rettiwt.dm.inbox(); + output(inbox); + } catch (error) { + output(error); + } + }); + + // Inbox timeline + dm.command('inbox-timeline') + .description('Get more conversations from the inbox timeline (for pagination)') + .argument('[cursor]', 'The cursor to the batch of conversations to fetch (maxId from previous response)') + .action(async (cursor?: string) => { + try { + const inbox = await rettiwt.dm.inboxTimeline(cursor); + output(inbox); + } catch (error) { + output(error); + } + }); + + return dm; +} + +export default createDirectMessageCommand; \ No newline at end of file From 9327fb583495f298fe7f19a41bfa6d00c9ebfa41 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 18 Jul 2025 18:30:04 +0000 Subject: [PATCH 058/119] Formatted code --- src/commands/DirectMessage.ts | 2 +- src/models/data/CursoredData.ts | 4 +--- src/models/data/DirectMessage.ts | 2 +- src/services/public/DirectMessageService.ts | 8 ++++---- src/types/args/PostArgs.ts | 2 +- src/types/raw/dm/Message.ts | 12 ++++++------ 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/commands/DirectMessage.ts b/src/commands/DirectMessage.ts index 9fe1b069..735b2067 100644 --- a/src/commands/DirectMessage.ts +++ b/src/commands/DirectMessage.ts @@ -68,4 +68,4 @@ function createDirectMessageCommand(rettiwt: Rettiwt): Command { return dm; } -export default createDirectMessageCommand; \ No newline at end of file +export default createDirectMessageCommand; diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index 67aa1e51..b70bfd7f 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -16,9 +16,7 @@ import { User } from './User'; * * @public */ -export class CursoredData - implements ICursoredData -{ +export class CursoredData implements ICursoredData { public list: T[]; public next: string; diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts index 5dc46c21..d14e1b66 100644 --- a/src/models/data/DirectMessage.ts +++ b/src/models/data/DirectMessage.ts @@ -1,5 +1,5 @@ import { IDirectMessage } from '../../types/data/DirectMessage'; -import { IConversationTimelineResponse} from '../../types/raw/dm/Conversation'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; import { IRawMessageBase } from '../../types/raw/dm/Message'; diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index 704a8b13..6748016c 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -72,15 +72,15 @@ export class DirectMessageService extends FetcherService { /** * Delete a conversation. * You will leave the conversation and it will be removed from your inbox. - * + * * @param conversationId - The ID of the conversation to delete. - * + * * @returns A promise that resolves when the conversation is deleted. - * + * * @example * ```ts * import { Rettiwt } from 'rettiwt-api'; - * + * * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * // Deleting a conversation diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index a36e0726..bc33053a 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -52,7 +52,7 @@ export interface IPostArgs { /** * The id of the conversation to delete. - * + * * @remarks * Required only when deleting a conversation using {@link ResourceType.DM_DELETE_CONVERSATION} */ diff --git a/src/types/raw/dm/Message.ts b/src/types/raw/dm/Message.ts index bd3a5cf8..2121190d 100644 --- a/src/types/raw/dm/Message.ts +++ b/src/types/raw/dm/Message.ts @@ -12,11 +12,11 @@ type UnifiedMessageData = InboxMessageData & Partial & /* eslint-disable @typescript-eslint/naming-convention */ export interface IRawMessageBase { - id: string; - time: string; - affects_sort?: boolean; - request_id: string; - conversation_id: string; - message_data: UnifiedMessageData; + id: string; + time: string; + affects_sort?: boolean; + request_id: string; + conversation_id: string; + message_data: UnifiedMessageData; } /* eslint-enable @typescript-eslint/naming-convention */ From 41b9359b68a9cd208c56579b3e593585605adfa4 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 22 Jul 2025 14:02:40 +0000 Subject: [PATCH 059/119] Renamed some types --- src/models/data/DirectMessage.ts | 8 ++++---- src/types/raw/{dm => base}/Message.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) rename src/types/raw/{dm => base}/Message.ts (64%) diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts index d14e1b66..7a6dc76e 100644 --- a/src/models/data/DirectMessage.ts +++ b/src/models/data/DirectMessage.ts @@ -1,8 +1,8 @@ import { IDirectMessage } from '../../types/data/DirectMessage'; +import { IMessage as IRawMessage } from '../../types/raw/base/Message'; import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; -import { IRawMessageBase } from '../../types/raw/dm/Message'; /** * Type guard to check if the response is an IInboxInitialResponse @@ -38,7 +38,7 @@ function isInboxTimelineResponse( */ export class DirectMessage implements IDirectMessage { /** The raw message details. */ - private readonly _raw: IRawMessageBase; + private readonly _raw: IRawMessage; public conversationId: string; public createdAt: string; @@ -54,7 +54,7 @@ export class DirectMessage implements IDirectMessage { * @param message - The raw message details from the API response. */ public constructor(message: unknown) { - this._raw = message as IRawMessageBase; + this._raw = message as IRawMessage; const parsedData = this._parseMessageData(message); @@ -70,7 +70,7 @@ export class DirectMessage implements IDirectMessage { } /** The raw message details. */ - public get raw(): IRawMessageBase { + public get raw(): IRawMessage { return this._raw; } diff --git a/src/types/raw/dm/Message.ts b/src/types/raw/base/Message.ts similarity index 64% rename from src/types/raw/dm/Message.ts rename to src/types/raw/base/Message.ts index 2121190d..844a2bce 100644 --- a/src/types/raw/dm/Message.ts +++ b/src/types/raw/base/Message.ts @@ -1,6 +1,8 @@ -import { ConversationMessage } from './Conversation'; -import { Message } from './InboxInitial'; -import { TimelineMessage } from './InboxTimeline'; +/* eslint-disable */ + +import { ConversationMessage } from '../dm/Conversation'; +import { Message } from '../dm/InboxInitial'; +import { TimelineMessage } from '../dm/InboxTimeline'; // Extract the message_data types type ConversationMessageData = ConversationMessage['message_data']; @@ -10,8 +12,7 @@ type TimelineMessageData = TimelineMessage['message_data']; // Create unified message_data type that includes all possible fields type UnifiedMessageData = InboxMessageData & Partial & Partial; -/* eslint-disable @typescript-eslint/naming-convention */ -export interface IRawMessageBase { +export interface IMessage { id: string; time: string; affects_sort?: boolean; @@ -19,4 +20,3 @@ export interface IRawMessageBase { conversation_id: string; message_data: UnifiedMessageData; } -/* eslint-enable @typescript-eslint/naming-convention */ From 8a2840b752308422cd62efa5014e435848d89443 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 22 Jul 2025 14:04:10 +0000 Subject: [PATCH 060/119] Fixed some typos --- src/commands/List.ts | 2 +- src/commands/Tweet.ts | 2 +- src/commands/User.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/List.ts b/src/commands/List.ts index d91ef51d..a9ecefe8 100644 --- a/src/commands/List.ts +++ b/src/commands/List.ts @@ -11,7 +11,7 @@ import { Rettiwt } from '../Rettiwt'; */ function createListCommand(rettiwt: Rettiwt): Command { // Creating the 'list' command - const list = createCommand('list').description('Access resources releated to lists'); + const list = createCommand('list').description('Access resources related to lists'); // Add member list.command('add-member') diff --git a/src/commands/Tweet.ts b/src/commands/Tweet.ts index 731331cf..bce6d4fe 100644 --- a/src/commands/Tweet.ts +++ b/src/commands/Tweet.ts @@ -14,7 +14,7 @@ import { ITweetFilter } from '../types/args/FetchArgs'; */ function createTweetCommand(rettiwt: Rettiwt): Command { // Creating the 'tweet' command - const tweet = createCommand('tweet').description('Access resources releated to tweets'); + const tweet = createCommand('tweet').description('Access resources related to tweets'); // Bookmark tweet diff --git a/src/commands/User.ts b/src/commands/User.ts index 8f7d5d4c..e65cd223 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -12,7 +12,7 @@ import { Rettiwt } from '../Rettiwt'; */ function createUserCommand(rettiwt: Rettiwt): Command { // Creating the 'user' command - const user = createCommand('user').description('Access resources releated to users'); + const user = createCommand('user').description('Access resources related to users'); // Affiliates user.command('affiliates') From 36ef24b52b22e92785277d8e2f9edbe0e3400542 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 22 Jul 2025 14:05:28 +0000 Subject: [PATCH 061/119] Updated exports --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 14344c74..2881aed2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ export { IErrorData as IRawErrorData, IErrorDetails as IRawErrorDetails } from ' export { ILimitedVisibilityTweet as IRawLimitedVisibilityTweet } from './types/raw/base/LimitedVisibilityTweet'; export { IList as IRawList } from './types/raw/base/List'; export { IMedia as IRawMedia } from './types/raw/base/Media'; +export { IMessage as IRawMessage } from './types/raw/base/Message'; export { INotification as IRawNotification } from './types/raw/base/Notification'; export { ISpace as IRawSpace } from './types/raw/base/Space'; export { ITweet as IRawTweet } from './types/raw/base/Tweet'; From f4e733eb6b4b0474f8cd490226b755c8a058c871 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 22 Jul 2025 14:23:27 +0000 Subject: [PATCH 062/119] Removed inboxTimeline method from DM service and merge it's functionality with inbox --- src/commands/DirectMessage.ts | 21 ++---- src/services/public/DirectMessageService.ts | 72 +++++++-------------- 2 files changed, 31 insertions(+), 62 deletions(-) diff --git a/src/commands/DirectMessage.ts b/src/commands/DirectMessage.ts index 735b2067..f152c725 100644 --- a/src/commands/DirectMessage.ts +++ b/src/commands/DirectMessage.ts @@ -42,23 +42,14 @@ function createDirectMessageCommand(rettiwt: Rettiwt): Command { // Inbox dm.command('inbox') - .description('Get the initial state of the DM inbox, including recent conversations and messages') - .action(async () => { - try { - const inbox = await rettiwt.dm.inbox(); - output(inbox); - } catch (error) { - output(error); - } - }); - - // Inbox timeline - dm.command('inbox-timeline') - .description('Get more conversations from the inbox timeline (for pagination)') - .argument('[cursor]', 'The cursor to the batch of conversations to fetch (maxId from previous response)') + .description('Get your DM inbox') + .argument( + '[cursor]', + 'The cursor to the batch of conversations to fetch. If not provided, initial inbox is fetched', + ) .action(async (cursor?: string) => { try { - const inbox = await rettiwt.dm.inboxTimeline(cursor); + const inbox = await rettiwt.dm.inbox(cursor); output(inbox); } catch (error) { output(error); diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index 6748016c..fed3bd06 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -103,10 +103,11 @@ export class DirectMessageService extends FetcherService { } /** - * Get the initial state of the DM inbox, including recent conversations and messages. - * This is the main entry point for the DM system following the "Inbox as Entry Point" pattern. + * Get your inbox. * - * @returns The initial DM inbox state with conversations containing preview messages. + * @param cursor - The cursor to the inbox items to fetch. If not provided, intial inbox with most recent conversations is fetched. + * + * @returns The required inbox. Returns initial inbox if no cursor is provided. * * @example * @@ -127,55 +128,32 @@ export class DirectMessageService extends FetcherService { * }); * ``` */ - public async inbox(): Promise { - const resource = ResourceType.DM_INBOX_INITIAL_STATE; - - // Fetching raw inbox initial state - const response = await this.request(resource, {}); + public async inbox(cursor?: string): Promise { + // If cursor is provided, fetch initial inbox + if (cursor !== undefined) { + const resource = ResourceType.DM_INBOX_TIMELINE; - // Deserializing response - const data = Extractors[resource](response); + // Fetching raw inbox timeline + const response = await this.request(resource, { + maxId: cursor, + }); - return data; - } + // Deserializing response + const data = Extractors[resource](response); - /** - * Get more conversations from the inbox timeline (for pagination). - * Use this to load older conversations beyond what's included in the initial inbox state. - * - * @param cursor - The cursor to the batch of conversations to fetch (maxId from previous response). - * - * @returns The inbox timeline with older conversations. - * - * @example - * - * ```ts - * import { Rettiwt } from 'rettiwt-api'; - * - * // Creating a new Rettiwt instance using the given 'API_KEY' - * const rettiwt = new Rettiwt({ apiKey: API_KEY }); - * - * // Fetching older conversations using pagination - * rettiwt.dm.inboxTimeline('1803853649426133349') - * .then(inbox => { - * console.log(`Found ${inbox.conversations.length} additional conversations`); - * }) - * .catch(err => { - * console.log(err); - * }); - * ``` - */ - public async inboxTimeline(cursor?: string): Promise { - const resource = ResourceType.DM_INBOX_TIMELINE; + return data; + } + // Else, fetch next inbox data + else { + const resource = ResourceType.DM_INBOX_INITIAL_STATE; - // Fetching raw inbox timeline - const response = await this.request(resource, { - maxId: cursor, - }); + // Fetching raw inbox initial state + const response = await this.request(resource, {}); - // Deserializing response - const data = Extractors[resource](response); + // Deserializing response + const data = Extractors[resource](response); - return data; + return data; + } } } From e4f6d59cd9940939cff8de80a07becbfe1f0437e Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 22 Jul 2025 14:32:14 +0000 Subject: [PATCH 063/119] Updated README.md --- README.md | 3 +-- src/services/public/DirectMessageService.ts | 2 +- src/services/public/TweetService.ts | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61d009b1..f0452b99 100644 --- a/README.md +++ b/README.md @@ -445,9 +445,8 @@ So far, the following operations are supported: ### Direct Messages -- [Getting the initial DM inbox state](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inbox) +- [Getting the DM inbox](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inbox) - [Getting a specific conversation with full message history](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#conversation) -- [Getting more conversations from inbox timeline (pagination)](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inboxTimeline) - [Deleting a conversation](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#deleteConversation) ### List diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index fed3bd06..23461610 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -29,7 +29,7 @@ export class DirectMessageService extends FetcherService { * Use this to load complete message history for a conversation identified from the inbox. * * @param conversationId - The ID of the conversation (e.g., "394028042-1712730991884689408"). - * @param cursor - The cursor for pagination (maxId from previous response). + * @param cursor - The cursor for pagination. * * @returns The conversation with full message history, or undefined if not found. * diff --git a/src/services/public/TweetService.ts b/src/services/public/TweetService.ts index 87a8f42c..4df02515 100644 --- a/src/services/public/TweetService.ts +++ b/src/services/public/TweetService.ts @@ -356,6 +356,10 @@ export class TweetService extends FetcherService { /** * Get the list of replies to a tweet. * + * If the target tweet is a thread, + * the first batch always contains all the tweets in the thread, + * if `sortBy` is set to {@link TweetRepliesSortType.RELEVANCE}. + * * @param id - The ID of the target tweet. * @param cursor - The cursor to the batch of replies to fetch. * @param sortBy - The sorting order of the replies to fetch. Default is {@link TweetRepliesSortType.RECENT}. From b2848857627bb00cce4a398b16ac07a8de81f7c7 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 22 Jul 2025 17:35:17 +0200 Subject: [PATCH 064/119] feat: add playground --- eslint.config.mjs | 2 +- package-lock.json | 58 +++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++++- playground/.env.example | 1 + playground/README.md | 46 ++++++++++++++++++++++++++++++++ playground/index.js | 18 +++++++++++++ playground/package.json | 15 +++++++++++ tsconfig.json | 2 +- 8 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 playground/.env.example create mode 100644 playground/README.md create mode 100644 playground/index.js create mode 100644 playground/package.json diff --git a/eslint.config.mjs b/eslint.config.mjs index a4a2c836..60d7d728 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,7 @@ const compat = new FlatCompat({ export default [ { - ignores: ['dist/', 'node_modules/', 'docs/'], + ignores: ['dist/', 'node_modules/', 'docs/', 'playground/'], }, ...compat.extends('.eslintrc.js'), ]; diff --git a/package-lock.json b/package-lock.json index b2c12aa0..fe3bd938 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "rettiwt-api", "version": "6.0.5", "license": "ISC", + "workspaces": [ + "playground", + "src" + ], "dependencies": { "axios": "^1.8.4", "chalk": "^5.4.1", @@ -1411,6 +1415,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3705,6 +3721,31 @@ "node": ">=4" } }, + "node_modules/rettiwt-api": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/rettiwt-api/-/rettiwt-api-6.0.5.tgz", + "integrity": "sha512-YqFu6TXl58hCo+k8TGLca5/NM6nJJKCRYRhEgH67YtuPVrA6aFA71k+S7hVE+9rmzOKCBAEbFN/h1A/j2WazqA==", + "license": "ISC", + "dependencies": { + "axios": "^1.8.4", + "chalk": "^5.4.1", + "commander": "^11.1.0", + "cookiejar": "^2.1.4", + "https-proxy-agent": "^7.0.6", + "node-html-parser": "^7.0.1", + "x-client-transaction-id-glacier": "^1.0.0" + }, + "bin": { + "rettiwt": "dist/cli.js" + }, + "engines": { + "node": "^22.13.1" + } + }, + "node_modules/rettiwt-playground": { + "resolved": "playground", + "link": true + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4448,6 +4489,15 @@ "linkedom": "^0.18.9" } }, + "node_modules/x-client-transaction-id-glacier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/x-client-transaction-id-glacier/-/x-client-transaction-id-glacier-1.0.0.tgz", + "integrity": "sha512-LmRJHgLTkksatezztTO+52SpGcwYLf90O2r2t4L9leAlhM5lOv/03c5izupwswmQAFA4Uk0str4qEV34KhBUAw==", + "license": "MIT", + "dependencies": { + "linkedom": "^0.18.9" + } + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", @@ -4473,6 +4523,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "playground": { + "name": "rettiwt-playground", + "version": "1.0.0", + "dependencies": { + "dotenv": "^17.2.0", + "rettiwt-api": "workspace" + } } } } diff --git a/package.json b/package.json index 2015a890..8538a4b5 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,9 @@ "https-proxy-agent": "^7.0.6", "node-html-parser": "^7.0.1", "x-client-transaction-id": "^0.1.8" - } + }, + "workspaces": [ + "playground", + "src" + ] } diff --git a/playground/.env.example b/playground/.env.example new file mode 100644 index 00000000..f2fc89ed --- /dev/null +++ b/playground/.env.example @@ -0,0 +1 @@ +ACCESS_TOKEN="" \ No newline at end of file diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 00000000..8eeba85c --- /dev/null +++ b/playground/README.md @@ -0,0 +1,46 @@ +# Rettiwt Playground + +This playground is intended for developers to test and experiment with features from the Rettiwt-API package in a local development environment. + +## Getting Started + +### Prerequisites +- Node.js (v22 or higher recommended) +- npm (v7+ recommended for workspace support) + +### Setup +1. **Install dependencies** + From the root of the monorepo, run: + ```sh + npm install + ``` + This will install dependencies for all workspaces, including `playground` and `src`. + +2. **Environment Variables** + Create a `.env` file in the `playground` directory with your API credentials: + ```env + ACCESS_TOKEN=your_access_token_here + ``` + +### Usage +- The main entry point is [`index.js`](./index.js), which demonstrates usage of the Rettiwt-API. +- To run the playground: + ```sh + npm start --workspace=playground + ``` + or from the `playground` directory: + ```sh + npm start + ``` + +### Modifying Playground Code +- Edit `index.js` to try different API features or test new functionality. +- The `rettiwt-api` dependency is linked via npm workspaces, so changes in `src` are immediately available in the playground after rebuilding if necessary. + +## Notes +- This playground is for development and testing only. Do not use production credentials. +- For more advanced usage, add scripts or files as needed. + +--- + +For questions or issues, see the main project README or open an issue. diff --git a/playground/index.js b/playground/index.js new file mode 100644 index 00000000..dae15c6c --- /dev/null +++ b/playground/index.js @@ -0,0 +1,18 @@ +import { Rettiwt } from 'rettiwt-api'; +import { config } from 'dotenv'; + +config(); + +const rettiwt = new Rettiwt({ apiKey: process.env.ACCESS_TOKEN }); + +async function userDetails() { + try { + const user = await rettiwt.user.details(); + console.log(user); + } + catch (error) { + console.error('Error fetching user details:', error); + } +} + +await userDetails(); \ No newline at end of file diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 00000000..60a35dad --- /dev/null +++ b/playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "rettiwt-playground", + "version": "1.0.0", + "description": "A playground for testing Rettiwt-API features and functionalities.", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "test": "echo \"No tests specified\" && exit 0" + }, + "dependencies": { + "dotenv": "^17.2.0", + "rettiwt-api": "workspace" + } +} diff --git a/tsconfig.json b/tsconfig.json index 21ba8759..647354f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -93,5 +93,5 @@ // "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] + "exclude": ["node_modules", "**/*.spec.ts", "playground/*"] } From fe399e20ad22de74a851b0a30f174b14c1e2b5ee Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 22 Jul 2025 17:35:35 +0200 Subject: [PATCH 065/119] feat: improve PR and Issue examples --- .github/ISSUE_TEMPLATE/bug-report.yml | 57 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature-request.yml | 20 ++++++++ .github/ISSUE_TEMPLATE/question.yml | 15 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 31 ++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/question.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..29979316 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,57 @@ +name: "🐛 Bug report" +description: Report a bug to help us improve Rettiwt-API. +labels: ["triage", "bug"] +body: + - type: markdown + attributes: + value: | + Before reporting a bug, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: env + attributes: + label: Environment + description: Please provide your environment details. You can use `node -v` and `npm list rettiwt-api` to fill this section. + placeholder: | + - Operating System: `Windows/Linux/macOS` + - Node Version: `v22.x.x` (Rettiwt-API requires NodeJS 20+) + - Rettiwt-API Version: `x.x.x` + - Package Manager: `npm/yarn/pnpm` + - CLI or Dependency: `CLI` or `Dependency` + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Please provide a minimal code snippet or CLI command that reproduces the issue. If possible, include the API_KEY usage and any relevant configuration. If the report is vague and has no reproduction, it may be closed automatically. + placeholder: | + ```ts + import { Rettiwt } from 'rettiwt-api'; + const rettiwt = new Rettiwt({ apiKey: '' }); + // ... + ``` + or + ```sh + rettiwt -k + ``` + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, mention it here. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. + - type: textarea + id: logs + attributes: + label: Logs + description: | + Please copy-paste any error logs or stack traces here. Avoid screenshots if possible. + render: shell-script diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..89a53335 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,20 @@ +name: "🚀 Feature request (Rettiwt-API)" +description: Suggest an idea or enhancement for Rettiwt-API. +labels: ["triage", "enhancement", "feature-request"] +body: + - type: markdown + attributes: + value: | + Before requesting a feature, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the feature or enhancement. Include possible use cases, alternatives, and links to any prototype or related module. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..72c9b128 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,15 @@ +name: "💬 Question (Rettiwt-API)" +description: Ask a question about Rettiwt-API. +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Before asking a question, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: Please provide a clear and concise question. Include any relevant context, code snippets, or links to documentation. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..0d4afb03 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## 🔗 Related Issue + + + +## ❓ Type of Change + + +- [ ] 📖 Documentation (docs, README, or comments) +- [ ] 🐞 Bug fix (non-breaking fix for an issue) +- [ ] 👌 Enhancement (improvement to existing functionality) +- [ ] ✨ New feature (adds new functionality) +- [ ] 🧹 Chore (build, tooling, dependencies) +- [ ] ⚠️ Breaking change (affects existing usage) + +## 📚 Description + + + +## 📝 Checklist + +- [ ] Issue/discussion linked above. +- [ ] Documentation updated (if needed). +- [ ] Code follows project conventions and ESLint rules. +- [ ] No sensitive data or credentials are included. + + \ No newline at end of file From 268a666e50657bfa24fa90e959d578de73688048 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 22 Jul 2025 17:40:59 +0200 Subject: [PATCH 066/119] =?UTF-8?q?feat:=20mainly=20for=20me=20always=20fo?= =?UTF-8?q?rgetting=20to=20format=20and=20lint=20=F0=9F=A5=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9b488fb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: ci + +on: + push: + branches: + - dev +jobs: + ci: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + node: [22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install node + uses: actions/setup-node@v4 + + - name: Install dependencies + run: npm install + + - name: Run Format check + run: npm run format:check + + - name: Run Lint check + run: npm run lint:check + \ No newline at end of file diff --git a/package.json b/package.json index 8538a4b5..12477cc2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "build": "tsc", "prepare": "tsc", "format": "prettier --write .", + "format:check": "prettier --check .", "lint": "eslint --max-warnings 0 --fix .", + "lint:check": "eslint --max-warnings 0 .", "docs": "typedoc --excludePrivate --excludeProtected --excludeInternal src/index.ts", "debug": "nodemon ./dist/index.js --inspect=0.0.0.0:9229" }, From ed83d3792f6f6865e7f33b29886adb19cb06839d Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 22 Jul 2025 17:43:45 +0200 Subject: [PATCH 067/119] chore: ci on PR too --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b488fb9..1e7023aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: branches: - dev + pull_request: + branches: + - dev jobs: ci: runs-on: ${{ matrix.os }} From 44d11a63f0f52289806d144cc113858d267cf5fb Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Tue, 22 Jul 2025 17:44:48 +0200 Subject: [PATCH 068/119] chore: format --- .github/ISSUE_TEMPLATE/bug-report.yml | 110 ++++++++++----------- .github/ISSUE_TEMPLATE/feature-request.yml | 36 +++---- .github/ISSUE_TEMPLATE/question.yml | 26 ++--- .github/PULL_REQUEST_TEMPLATE.md | 3 +- .github/workflows/ci.yml | 49 +++++---- playground/README.md | 35 ++++--- playground/index.js | 15 ++- playground/package.json | 26 ++--- 8 files changed, 153 insertions(+), 147 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 29979316..7e806edd 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,57 +1,57 @@ -name: "🐛 Bug report" +name: '🐛 Bug report' description: Report a bug to help us improve Rettiwt-API. -labels: ["triage", "bug"] +labels: ['triage', 'bug'] body: - - type: markdown - attributes: - value: | - Before reporting a bug, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). - - type: textarea - id: env - attributes: - label: Environment - description: Please provide your environment details. You can use `node -v` and `npm list rettiwt-api` to fill this section. - placeholder: | - - Operating System: `Windows/Linux/macOS` - - Node Version: `v22.x.x` (Rettiwt-API requires NodeJS 20+) - - Rettiwt-API Version: `x.x.x` - - Package Manager: `npm/yarn/pnpm` - - CLI or Dependency: `CLI` or `Dependency` - validations: - required: true - - type: textarea - id: reproduction - attributes: - label: Reproduction - description: Please provide a minimal code snippet or CLI command that reproduces the issue. If possible, include the API_KEY usage and any relevant configuration. If the report is vague and has no reproduction, it may be closed automatically. - placeholder: | - ```ts - import { Rettiwt } from 'rettiwt-api'; - const rettiwt = new Rettiwt({ apiKey: '' }); - // ... - ``` - or - ```sh - rettiwt -k - ``` - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, mention it here. - validations: - required: true - - type: textarea - id: additional - attributes: - label: Additional context - description: If applicable, add any other context, configuration, or screenshots here. - - type: textarea - id: logs - attributes: - label: Logs - description: | - Please copy-paste any error logs or stack traces here. Avoid screenshots if possible. - render: shell-script + - type: markdown + attributes: + value: | + Before reporting a bug, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: env + attributes: + label: Environment + description: Please provide your environment details. You can use `node -v` and `npm list rettiwt-api` to fill this section. + placeholder: | + - Operating System: `Windows/Linux/macOS` + - Node Version: `v22.x.x` (Rettiwt-API requires NodeJS 20+) + - Rettiwt-API Version: `x.x.x` + - Package Manager: `npm/yarn/pnpm` + - CLI or Dependency: `CLI` or `Dependency` + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Please provide a minimal code snippet or CLI command that reproduces the issue. If possible, include the API_KEY usage and any relevant configuration. If the report is vague and has no reproduction, it may be closed automatically. + placeholder: | + ```ts + import { Rettiwt } from 'rettiwt-api'; + const rettiwt = new Rettiwt({ apiKey: '' }); + // ... + ``` + or + ```sh + rettiwt -k + ``` + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, mention it here. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. + - type: textarea + id: logs + attributes: + label: Logs + description: | + Please copy-paste any error logs or stack traces here. Avoid screenshots if possible. + render: shell-script diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 89a53335..9e79917a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,20 +1,20 @@ -name: "🚀 Feature request (Rettiwt-API)" +name: '🚀 Feature request (Rettiwt-API)' description: Suggest an idea or enhancement for Rettiwt-API. -labels: ["triage", "enhancement", "feature-request"] +labels: ['triage', 'enhancement', 'feature-request'] body: - - type: markdown - attributes: - value: | - Before requesting a feature, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). - - type: textarea - id: description - attributes: - label: Description - description: A clear and concise description of the feature or enhancement. Include possible use cases, alternatives, and links to any prototype or related module. - validations: - required: true - - type: textarea - id: additional - attributes: - label: Additional context - description: If applicable, add any other context, configuration, or screenshots here. + - type: markdown + attributes: + value: | + Before requesting a feature, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the feature or enhancement. Include possible use cases, alternatives, and links to any prototype or related module. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 72c9b128..f050283f 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,15 +1,15 @@ -name: "💬 Question (Rettiwt-API)" +name: '💬 Question (Rettiwt-API)' description: Ask a question about Rettiwt-API. -labels: ["question"] +labels: ['question'] body: - - type: markdown - attributes: - value: | - Before asking a question, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). - - type: textarea - id: description - attributes: - label: Description - description: Please provide a clear and concise question. Include any relevant context, code snippets, or links to documentation. - validations: - required: true + - type: markdown + attributes: + value: | + Before asking a question, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: Please provide a clear and concise question. Include any relevant context, code snippets, or links to documentation. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0d4afb03..2c2d621f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,7 @@ ## ❓ Type of Change + - [ ] 📖 Documentation (docs, README, or comments) - [ ] 🐞 Bug fix (non-breaking fix for an issue) - [ ] 👌 Enhancement (improvement to existing functionality) @@ -28,4 +29,4 @@ What problem does this solve? Any relevant context for Rettiwt-API? \ No newline at end of file +--> diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e7023aa..e9c8dc05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,34 +1,33 @@ name: ci -on: - push: - branches: - - dev - pull_request: - branches: - - dev +on: + push: + branches: + - dev + pull_request: + branches: + - dev jobs: - ci: - runs-on: ${{ matrix.os }} + ci: + runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - node: [22] + strategy: + matrix: + os: [ubuntu-latest] + node: [22] - steps: - - name: Checkout - uses: actions/checkout@v4 + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Install node - uses: actions/setup-node@v4 + - name: Install node + uses: actions/setup-node@v4 - - name: Install dependencies - run: npm install + - name: Install dependencies + run: npm install - - name: Run Format check - run: npm run format:check + - name: Run Format check + run: npm run format:check - - name: Run Lint check - run: npm run lint:check - \ No newline at end of file + - name: Run Lint check + run: npm run lint:check diff --git a/playground/README.md b/playground/README.md index 8eeba85c..b492eeb0 100644 --- a/playground/README.md +++ b/playground/README.md @@ -5,39 +5,46 @@ This playground is intended for developers to test and experiment with features ## Getting Started ### Prerequisites + - Node.js (v22 or higher recommended) - npm (v7+ recommended for workspace support) ### Setup + 1. **Install dependencies** From the root of the monorepo, run: - ```sh - npm install - ``` - This will install dependencies for all workspaces, including `playground` and `src`. + + ```sh + npm install + ``` + + This will install dependencies for all workspaces, including `playground` and `src`. 2. **Environment Variables** Create a `.env` file in the `playground` directory with your API credentials: - ```env - ACCESS_TOKEN=your_access_token_here - ``` + ```env + ACCESS_TOKEN=your_access_token_here + ``` ### Usage + - The main entry point is [`index.js`](./index.js), which demonstrates usage of the Rettiwt-API. - To run the playground: - ```sh - npm start --workspace=playground - ``` - or from the `playground` directory: - ```sh - npm start - ``` + ```sh + npm start --workspace=playground + ``` + or from the `playground` directory: + ```sh + npm start + ``` ### Modifying Playground Code + - Edit `index.js` to try different API features or test new functionality. - The `rettiwt-api` dependency is linked via npm workspaces, so changes in `src` are immediately available in the playground after rebuilding if necessary. ## Notes + - This playground is for development and testing only. Do not use production credentials. - For more advanced usage, add scripts or files as needed. diff --git a/playground/index.js b/playground/index.js index dae15c6c..4aa85ee4 100644 --- a/playground/index.js +++ b/playground/index.js @@ -6,13 +6,12 @@ config(); const rettiwt = new Rettiwt({ apiKey: process.env.ACCESS_TOKEN }); async function userDetails() { - try { - const user = await rettiwt.user.details(); - console.log(user); - } - catch (error) { - console.error('Error fetching user details:', error); - } + try { + const user = await rettiwt.user.details(); + console.log(user); + } catch (error) { + console.error('Error fetching user details:', error); + } } -await userDetails(); \ No newline at end of file +await userDetails(); diff --git a/playground/package.json b/playground/package.json index 60a35dad..69b6f957 100644 --- a/playground/package.json +++ b/playground/package.json @@ -1,15 +1,15 @@ { - "name": "rettiwt-playground", - "version": "1.0.0", - "description": "A playground for testing Rettiwt-API features and functionalities.", - "main": "index.js", - "type": "module", - "scripts": { - "start": "node index.js", - "test": "echo \"No tests specified\" && exit 0" - }, - "dependencies": { - "dotenv": "^17.2.0", - "rettiwt-api": "workspace" - } + "name": "rettiwt-playground", + "version": "1.0.0", + "description": "A playground for testing Rettiwt-API features and functionalities.", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "test": "echo \"No tests specified\" && exit 0" + }, + "dependencies": { + "dotenv": "^17.2.0", + "rettiwt-api": "workspace" + } } From 1828680ae0ed9ff0243522cc65abc955e99a4807 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 25 Jul 2025 16:17:26 +0200 Subject: [PATCH 069/119] refactor: playground index --- playground/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/playground/index.js b/playground/index.js index 4aa85ee4..791916cb 100644 --- a/playground/index.js +++ b/playground/index.js @@ -1,7 +1,5 @@ import { Rettiwt } from 'rettiwt-api'; -import { config } from 'dotenv'; - -config(); +import 'dotenv/config'; const rettiwt = new Rettiwt({ apiKey: process.env.ACCESS_TOKEN }); From f5d22bb4543ee6a0f90e8a5327bb1445f4961b9f Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 4 Aug 2025 16:10:25 +0000 Subject: [PATCH 070/119] Updated list type --- src/models/data/List.ts | 12 ++++++++++-- src/types/data/List.ts | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/models/data/List.ts b/src/models/data/List.ts index 076bcce8..014a310c 100644 --- a/src/models/data/List.ts +++ b/src/models/data/List.ts @@ -1,6 +1,8 @@ import { IList } from '../../types/data/List'; import { IList as IRawList } from '../../types/raw/base/List'; +import { User } from './User'; + /** * The details of a single Twitter List. * @@ -11,9 +13,11 @@ export class List implements IList { private readonly _raw: IRawList; public createdAt: string; - public createdBy: string; + public createdBy: User; public description?: string; public id: string; + public isFollowing: boolean; + public isMember: boolean; public memberCount: number; public name: string; public subscriberCount: number; @@ -27,9 +31,11 @@ export class List implements IList { this.name = list.name; this.createdAt = new Date(list.created_at).toISOString(); this.description = list.description.length ? list.description : undefined; + this.isFollowing = list.following; + this.isMember = list.is_member; this.memberCount = list.member_count; this.subscriberCount = list.subscriber_count; - this.createdBy = list.user_results.result.id; + this.createdBy = new User(list.user_results.result); } /** The raw list details. */ @@ -46,6 +52,8 @@ export class List implements IList { createdBy: this.createdBy, description: this.description, id: this.id, + isFollowing: this.isFollowing, + isMember: this.isMember, memberCount: this.memberCount, name: this.name, subscriberCount: this.subscriberCount, diff --git a/src/types/data/List.ts b/src/types/data/List.ts index a16090b0..9faca9c4 100644 --- a/src/types/data/List.ts +++ b/src/types/data/List.ts @@ -1,3 +1,5 @@ +import { IUser } from './User'; + /** * The details of a single Twitter List. * @@ -7,15 +9,21 @@ export interface IList { /** The date and time of creation of the list, int UTC string format. */ createdAt: string; - /** The rest id of the user who created the list. */ - createdBy: string; + /** The user who created the list. */ + createdBy: IUser; /** The list description. */ description?: string; + /** Whether the user is following the list or not. */ + isFollowing: boolean; + /** The rest id of the list. */ id: string; + /** Whether the user is a member of the list or not. */ + isMember: boolean; + /** The number of memeber of the list. */ memberCount: number; From 920115f27fdecc1064476b051e19d78ad12cb8c8 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 4 Aug 2025 16:35:18 +0000 Subject: [PATCH 071/119] Added ability to get the details of list --- README.md | 2 + src/collections/Extractors.ts | 3 ++ src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/List.ts | 13 ++++++ src/enums/Resource.ts | 1 + src/models/data/List.ts | 20 +++++++++ src/services/public/ListService.ts | 43 ++++++++++++++++++++ src/types/raw/list/Details.ts | 65 ++++++++++++++++++++++++------ 9 files changed, 136 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f0452b99..bf893362 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - Direct Message Conversations - Direct Message Delete Conversation - List Add Member + - List Details - List Members - List Remove Member - List Tweets @@ -452,6 +453,7 @@ So far, the following operations are supported: ### List - [Adding a member to a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#addMember) +- [Getting the details of a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#details) - [Getting the members of a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#members) - [Removing a member from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#removeMember) - [Getting the list of tweets from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#tweets) diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index f35e5297..736d0dc7 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -3,6 +3,7 @@ import { Analytics } from '../models/data/Analytics'; import { Conversation } from '../models/data/Conversation'; import { CursoredData } from '../models/data/CursoredData'; import { Inbox } from '../models/data/Inbox'; +import { List } from '../models/data/List'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; import { User } from '../models/data/User'; @@ -10,6 +11,7 @@ import { IConversationTimelineResponse } from '../types/raw/dm/Conversation'; import { IInboxInitialResponse } from '../types/raw/dm/InboxInitial'; import { IInboxTimelineResponse } from '../types/raw/dm/InboxTimeline'; import { IListMemberAddResponse } from '../types/raw/list/AddMember'; +import { IListDetailsResponse } from '../types/raw/list/Details'; import { IListMembersResponse } from '../types/raw/list/Members'; import { IListMemberRemoveResponse } from '../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; @@ -57,6 +59,7 @@ import { IUserUnfollowResponse } from '../types/raw/user/Unfollow'; export const Extractors = { /* eslint-disable @typescript-eslint/naming-convention */ + LIST_DETAILS: (response: IListDetailsResponse, id: string): List | undefined => List.single(response, id), LIST_MEMBERS: (response: IListMembersResponse): CursoredData => new CursoredData(response, BaseType.USER), LIST_MEMBER_ADD: (response: IListMemberAddResponse): number | undefined => diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 2cb16a65..34b80374 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -17,6 +17,7 @@ export const AllowGuestAuthenticationGroup = [ * @internal */ export const FetchResourcesGroup = [ + ResourceType.LIST_DETAILS, ResourceType.LIST_MEMBERS, ResourceType.LIST_TWEETS, ResourceType.DM_CONVERSATION, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 068fe4c6..f7be41f2 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -19,6 +19,7 @@ import { TweetRepliesSortTypeMap } from './Tweet'; export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | IPostArgs) => AxiosRequestConfig } = { /* eslint-disable @typescript-eslint/naming-convention */ + LIST_DETAILS: (args: IFetchArgs) => ListRequests.details(args.id!), LIST_MEMBERS: (args: IFetchArgs) => ListRequests.members(args.id!, args.count, args.cursor), LIST_MEMBER_ADD: (args: IPostArgs) => ListRequests.addMember(args.id!, args.userId!), LIST_MEMBER_REMOVE: (args: IPostArgs) => ListRequests.removeMember(args.id!, args.userId!), diff --git a/src/commands/List.ts b/src/commands/List.ts index a9ecefe8..e074bc25 100644 --- a/src/commands/List.ts +++ b/src/commands/List.ts @@ -27,6 +27,19 @@ function createListCommand(rettiwt: Rettiwt): Command { } }); + // Details + list.command('details') + .description('Fetch the details of a list') + .argument('', 'The ID of the tweet list') + .action(async (id: string) => { + try { + const details = await rettiwt.list.details(id); + output(details); + } catch (error) { + output(error); + } + }); + // Members list.command('members') .description('Fetch the list of members of the given tweet list') diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index c18e7445..f51c32f1 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -5,6 +5,7 @@ */ export enum ResourceType { // LIST + LIST_DETAILS = 'LIST_DETAILS', LIST_MEMBER_ADD = 'LIST_MEMBER_ADD', LIST_MEMBER_REMOVE = 'LIST_MEMBER_REMOVE', LIST_MEMBERS = 'LIST_MEMBERS', diff --git a/src/models/data/List.ts b/src/models/data/List.ts index 014a310c..e88062cf 100644 --- a/src/models/data/List.ts +++ b/src/models/data/List.ts @@ -1,5 +1,6 @@ import { IList } from '../../types/data/List'; import { IList as IRawList } from '../../types/raw/base/List'; +import { IListDetailsResponse } from '../../types/raw/list/Details'; import { User } from './User'; @@ -43,6 +44,25 @@ export class List implements IList { return { ...this._raw }; } + /** + * Extracts and deserializes a single target list from the given raw response data. + * + * @param response - The raw response data. + * @param id - The id of the target list. + * + * @returns The target deserialized list. + */ + public static single(response: IListDetailsResponse, id: string): List | undefined { + // If list found + if (response.data.list.id_str === id) { + return new List(response.data.list as unknown as IRawList); + } + // If not found + else { + return undefined; + } + } + /** * @returns A serializable JSON representation of `this` object. */ diff --git a/src/services/public/ListService.ts b/src/services/public/ListService.ts index 35caf913..72eaaabe 100644 --- a/src/services/public/ListService.ts +++ b/src/services/public/ListService.ts @@ -1,10 +1,12 @@ import { Extractors } from '../../collections/Extractors'; import { ResourceType } from '../../enums/Resource'; import { CursoredData } from '../../models/data/CursoredData'; +import { List } from '../../models/data/List'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IListMemberAddResponse } from '../../types/raw/list/AddMember'; +import { IListDetailsResponse } from '../../types/raw/list/Details'; import { IListMembersResponse } from '../../types/raw/list/Members'; import { IListMemberRemoveResponse } from '../../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../../types/raw/list/Tweets'; @@ -62,6 +64,47 @@ export class ListService extends FetcherService { return data; } + /** + * Get the details of a list. + * + * @param id - The ID of the target list. + * + * @returns + * The details of the target list. + * + * If list not found, returns undefined. + * + * @example + * + * #### Fetching the details of a list + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the details of the list with the id '1234567890' + * rettiwt.list.details('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async details(id: string): Promise { + const resource: ResourceType = ResourceType.LIST_DETAILS; + + // Getting the details of the list + const response = await this.request(resource, { id: id }); + + // Deserializing response + const data = Extractors[resource](response, id); + + return data; + } + /** * Get the list of members of a tweet list. * diff --git a/src/types/raw/list/Details.ts b/src/types/raw/list/Details.ts index 40812484..1c70c91c 100644 --- a/src/types/raw/list/Details.ts +++ b/src/types/raw/list/Details.ts @@ -18,19 +18,20 @@ interface List { default_banner_media: DefaultBannerMedia; default_banner_media_results: DefaultBannerMediaResults; description: string; + facepile_urls: string[]; + followers_context: string; following: boolean; id: string; id_str: string; is_member: boolean; member_count: number; + members_context: string; mode: string; muting: boolean; name: string; + pinning: boolean; subscriber_count: number; user_results: UserResults; - facepile_urls: string[]; - followers_context: string; - members_context: string; } interface DefaultBannerMedia { @@ -87,19 +88,40 @@ interface Result2 { id: string; rest_id: string; affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; has_graduated_access: boolean; is_blue_verified: boolean; - profile_image_shape: string; legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; verified_phone_status: boolean; } interface AffiliatesHighlightedLabel {} -interface Legacy { - can_dm: boolean; - can_media_tag: boolean; +interface Avatar { + image_url: string; +} + +interface Core { created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { default_profile: boolean; default_profile_image: boolean; description: string; @@ -111,19 +133,14 @@ interface Legacy { has_custom_timelines: boolean; is_translator: boolean; listed_count: number; - location: string; media_count: number; - name: string; normal_followers_count: number; - pinned_tweet_ids_str: any[]; + pinned_tweet_ids_str: string[]; possibly_sensitive: boolean; profile_banner_url: string; - profile_image_url_https: string; profile_interstitial_type: string; - screen_name: string; statuses_count: number; translator_type: string; - verified: boolean; want_retweets: boolean; withheld_in_countries: any[]; } @@ -135,3 +152,25 @@ interface Entities { interface Description { urls: any[]; } + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + following: boolean; +} + +interface TipjarSettings {} + +interface Verification { + verified: boolean; +} From 22c705607b422b020c5f5afd99e195359bd93d44 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 6 Aug 2025 15:37:47 +0000 Subject: [PATCH 072/119] Added ability to fetch logged-in user's lists --- README.md | 2 + src/collections/Extractors.ts | 2 + src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/User.ts | 14 + src/enums/Data.ts | 1 + src/enums/Resource.ts | 1 + src/models/data/CursoredData.ts | 9 +- src/models/data/List.ts | 39 ++- src/requests/User.ts | 55 ++++ src/services/public/UserService.ts | 44 +++ src/types/data/CursoredData.ts | 3 +- src/types/data/List.ts | 6 +- src/types/raw/composite/TimelineList.ts | 10 + src/types/raw/user/Lists.ts | 378 ++++++++++++++++++++++++ 15 files changed, 555 insertions(+), 11 deletions(-) create mode 100644 src/types/raw/composite/TimelineList.ts create mode 100644 src/types/raw/user/Lists.ts diff --git a/README.md b/README.md index bf893362..772b6b82 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Following - User Highlights - User Likes + - User Lists - User Media - User Notification - User Recommended Feed @@ -490,6 +491,7 @@ So far, the following operations are supported: - [Getting the list of users who are followed by the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#following) - [Getting the list of highlighted tweets of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#highlights) - [Getting the list of tweets liked by the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#likes) +- [Getting the lists of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#lists) - [Getting the media timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#media) - [Streaming notifications of the logged-in user in pseudo-realtime](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#notifications) - [Getting the recommended feed of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#recommended) diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 736d0dc7..becd90f1 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -43,6 +43,7 @@ import { IUserFollowersResponse } from '../types/raw/user/Followers'; import { IUserFollowingResponse } from '../types/raw/user/Following'; import { IUserHighlightsResponse } from '../types/raw/user/Highlights'; import { IUserLikesResponse } from '../types/raw/user/Likes'; +import { IUserListsResponse } from '../types/raw/user/Lists'; import { IUserMediaResponse } from '../types/raw/user/Media'; import { IUserNotificationsResponse } from '../types/raw/user/Notifications'; import { IUserRecommendedResponse } from '../types/raw/user/Recommended'; @@ -124,6 +125,7 @@ export const Extractors = { new CursoredData(response, BaseType.USER), USER_HIGHLIGHTS: (response: IUserHighlightsResponse): CursoredData => new CursoredData(response, BaseType.TWEET), + USER_LISTS: (response: IUserListsResponse): CursoredData => new CursoredData(response, BaseType.LIST), USER_LIKES: (response: IUserLikesResponse): CursoredData => new CursoredData(response, BaseType.TWEET), USER_MEDIA: (response: IUserMediaResponse): CursoredData => diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 34b80374..77657ebe 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -42,6 +42,7 @@ export const FetchResourcesGroup = [ ResourceType.USER_FOLLOWERS, ResourceType.USER_HIGHLIGHTS, ResourceType.USER_LIKES, + ResourceType.USER_LISTS, ResourceType.USER_MEDIA, ResourceType.USER_NOTIFICATIONS, ResourceType.USER_SUBSCRIPTIONS, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index f7be41f2..b3439db6 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -73,6 +73,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | USER_FOLLOWERS: (args: IFetchArgs) => UserRequests.followers(args.id!, args.count, args.cursor), USER_HIGHLIGHTS: (args: IFetchArgs) => UserRequests.highlights(args.id!, args.count, args.cursor), USER_LIKES: (args: IFetchArgs) => UserRequests.likes(args.id!, args.count, args.cursor), + USER_LISTS: (args: IFetchArgs) => UserRequests.lists(args.id!, args.count, args.cursor), USER_MEDIA: (args: IFetchArgs) => UserRequests.media(args.id!, args.count, args.cursor), USER_NOTIFICATIONS: (args: IFetchArgs) => UserRequests.notifications(args.count, args.cursor), USER_SUBSCRIPTIONS: (args: IFetchArgs) => UserRequests.subscriptions(args.id!, args.count, args.cursor), diff --git a/src/commands/User.ts b/src/commands/User.ts index e65cd223..dae5c5b6 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -189,6 +189,20 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Lists + user.command('lists') + .description('Fetch your lists') + .argument('[count]', 'The number of lists to fetch') + .argument('[cursor]', 'The cursor to the batch of lists to fetch') + .action(async (count?: string, cursor?: string) => { + try { + const lists = await rettiwt.user.lists(count ? parseInt(count) : undefined, cursor); + output(lists); + } catch (error) { + output(error); + } + }); + // Media user.command('media') .description('Fetch the media timeline the given user') diff --git a/src/enums/Data.ts b/src/enums/Data.ts index 29f98455..1ea170ae 100644 --- a/src/enums/Data.ts +++ b/src/enums/Data.ts @@ -8,4 +8,5 @@ export enum BaseType { NOTIFICATION = 'NOTIFICATION', TWEET = 'TWEET', USER = 'USER', + LIST = 'LIST', } diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index f51c32f1..c8894fc3 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -55,6 +55,7 @@ export enum ResourceType { USER_FOLLOWERS = 'USER_FOLLOWERS', USER_HIGHLIGHTS = 'USER_HIGHLIGHTS', USER_LIKES = 'USER_LIKES', + USER_LISTS = 'USER_LISTS', USER_MEDIA = 'USER_MEDIA', USER_NOTIFICATIONS = 'USER_NOTIFICATIONS', USER_SUBSCRIPTIONS = 'USER_SUBSCRIPTIONS', diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index b70bfd7f..8b46682c 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -5,6 +5,8 @@ import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; +import { List } from './List'; + import { Notification } from './Notification'; import { Tweet } from './Tweet'; import { User } from './User'; @@ -16,7 +18,7 @@ import { User } from './User'; * * @public */ -export class CursoredData implements ICursoredData { +export class CursoredData implements ICursoredData { public list: T[]; public next: string; @@ -35,9 +37,12 @@ export class CursoredData implements ICur } else if (type == BaseType.USER) { this.list = User.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; + } else if (type == BaseType.LIST) { + this.list = List.timeline(response) as T[]; + this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; } else if (type == BaseType.NOTIFICATION) { this.list = Notification.list(response) as T[]; - this.next = findByFilter(response, 'cursorType', 'Top')[0]?.value ?? ''; + this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; } } diff --git a/src/models/data/List.ts b/src/models/data/List.ts index e88062cf..11a1924b 100644 --- a/src/models/data/List.ts +++ b/src/models/data/List.ts @@ -1,9 +1,11 @@ +import { LogActions } from '../../enums/Logging'; +import { findByFilter } from '../../helper/JsonUtils'; +import { LogService } from '../../services/internal/LogService'; import { IList } from '../../types/data/List'; import { IList as IRawList } from '../../types/raw/base/List'; +import { ITimelineList } from '../../types/raw/composite/TimelineList'; import { IListDetailsResponse } from '../../types/raw/list/Details'; -import { User } from './User'; - /** * The details of a single Twitter List. * @@ -14,7 +16,7 @@ export class List implements IList { private readonly _raw: IRawList; public createdAt: string; - public createdBy: User; + public createdBy: string; public description?: string; public id: string; public isFollowing: boolean; @@ -36,7 +38,7 @@ export class List implements IList { this.isMember = list.is_member; this.memberCount = list.member_count; this.subscriberCount = list.subscriber_count; - this.createdBy = new User(list.user_results.result); + this.createdBy = list.user_results.result.rest_id; } /** The raw list details. */ @@ -63,6 +65,35 @@ export class List implements IList { } } + /** + * Extracts and deserializes the timeline of lists followed from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized timeline of lists. + */ + public static timeline(response: NonNullable): List[] { + const lists: List[] = []; + + // Extracting the matching data + const extract = findByFilter(response, '__typename', 'TimelineTwitterList').map( + (item) => item.list, + ); + + // Deserializing valid data + for (const item of extract) { + // If valid list + if (item !== undefined && item.id !== undefined && item.following === true) { + // Logging + LogService.log(LogActions.DESERIALIZE, { id: item.id }); + + lists.push(new List(item)); + } + } + + return lists; + } + /** * @returns A serializable JSON representation of `this` object. */ diff --git a/src/requests/User.ts b/src/requests/User.ts index 10cbc38b..438ed40c 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -547,6 +547,61 @@ export class UserRequests { }; } + /** + * @param id - The id of the user whose lists are to be fetched. + * @param count - The number of lists to fetch. Only works as a lower limit when used with a cursor. + * @param cursor - The cursor to the batch of lists to fetch. + */ + public static lists(id: string, count?: number, cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/tZg2CHWw-NAL0nKO2Q-P4Q/ListsManagementPageTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ count: 100, cursor: cursor }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + payments_enabled: false, + rweb_xchat_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }), + fieldToggles: { withArticlePlainText: false }, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + /** * @param id - The id of the user whose media is to be fetched. * @param count - The number of media to fetch. Only works as a lower limit when used with a cursor. diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index e71075a0..13f1a133 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -3,6 +3,7 @@ import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Ana import { ResourceType } from '../../enums/Resource'; import { Analytics } from '../../models/data/Analytics'; import { CursoredData } from '../../models/data/CursoredData'; +import { List } from '../../models/data/List'; import { Notification } from '../../models/data/Notification'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; @@ -18,6 +19,7 @@ import { IUserFollowersResponse } from '../../types/raw/user/Followers'; import { IUserFollowingResponse } from '../../types/raw/user/Following'; import { IUserHighlightsResponse } from '../../types/raw/user/Highlights'; import { IUserLikesResponse } from '../../types/raw/user/Likes'; +import { IUserListsResponse } from '../../types/raw/user/Lists'; import { IUserMediaResponse } from '../../types/raw/user/Media'; import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; import { IUserRecommendedResponse } from '../../types/raw/user/Recommended'; @@ -576,6 +578,48 @@ export class UserService extends FetcherService { return data; } + /** + * Get the list of of the the logged in user. Includes both followed and owned. + * + * @param count - The number of lists to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of likes to fetch. + * + * @returns The list of tweets liked by the target user. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the first 100 Lists of the logged in User + * rettiwt.user.likes() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async lists(count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_LISTS; + + // Fetching raw list of lists + const response = await this.request(resource, { + id: this.config.userId, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + /** * Get the media timeline of a user. * diff --git a/src/types/data/CursoredData.ts b/src/types/data/CursoredData.ts index 2f2c59af..34d0c7e0 100644 --- a/src/types/data/CursoredData.ts +++ b/src/types/data/CursoredData.ts @@ -1,5 +1,6 @@ import { IConversation } from './Conversation'; import { IDirectMessage } from './DirectMessage'; +import { IList } from './List'; import { INotification } from './Notification'; import { ITweet } from './Tweet'; import { IUser } from './User'; @@ -11,7 +12,7 @@ import { IUser } from './User'; * * @public */ -export interface ICursoredData { +export interface ICursoredData { /** The batch of data of the given type. */ list: T[]; diff --git a/src/types/data/List.ts b/src/types/data/List.ts index 9faca9c4..7e02ebfd 100644 --- a/src/types/data/List.ts +++ b/src/types/data/List.ts @@ -1,5 +1,3 @@ -import { IUser } from './User'; - /** * The details of a single Twitter List. * @@ -9,8 +7,8 @@ export interface IList { /** The date and time of creation of the list, int UTC string format. */ createdAt: string; - /** The user who created the list. */ - createdBy: IUser; + /** The ID of the user who created the list. */ + createdBy: string; /** The list description. */ description?: string; diff --git a/src/types/raw/composite/TimelineList.ts b/src/types/raw/composite/TimelineList.ts new file mode 100644 index 00000000..915f4226 --- /dev/null +++ b/src/types/raw/composite/TimelineList.ts @@ -0,0 +1,10 @@ +import { IList } from '../base/List'; + +/** + * Represents the raw data of a single timeline tweet. + * + * @public + */ +export interface ITimelineList { + list: IList; +} diff --git a/src/types/raw/user/Lists.ts b/src/types/raw/user/Lists.ts new file mode 100644 index 00000000..6a3bd702 --- /dev/null +++ b/src/types/raw/user/Lists.ts @@ -0,0 +1,378 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching the lists of the given user. + * + * @public + */ +export interface IUserListsResponse { + data: Data; +} + +export interface Data { + viewer: Viewer; +} + +export interface Viewer { + list_management_timeline: ListManagementTimeline; +} + +export interface ListManagementTimeline { + timeline: Timeline; +} + +export interface Timeline { + instructions: Instruction[]; + metadata: Metadata; +} + +export interface Instruction { + type: string; + direction?: string; + entries?: Entry[]; +} + +export interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +export interface Content { + entryType: string; + __typename: string; + items?: Item[]; + displayType?: string; + header?: Header; + footer?: Footer; + clientEventInfo?: ClientEventInfo2; + value?: string; + cursorType?: string; +} + +export interface Item { + entryId: string; + item: Item2; +} + +export interface Item2 { + itemContent: ItemContent; + clientEventInfo: ClientEventInfo; +} + +export interface ItemContent { + itemType: string; + __typename: string; + displayType: string; + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: string[]; + followers_context?: string; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + members_context?: string; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; + custom_banner_media?: CustomBannerMedia; + custom_banner_media_results?: CustomBannerMediaResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + professional?: Professional; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + pinned_tweet_ids_str: string[]; + possibly_sensitive: boolean; + profile_banner_url?: string; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + url?: string; + want_retweets: boolean; + withheld_in_countries: any[]; + needs_phone_verification?: boolean; +} + +export interface Entities { + description: Description; + url?: Url; +} + +export interface Description { + urls: any[]; +} + +export interface Url { + urls: Url2[]; +} + +export interface Url2 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Professional { + rest_id: string; + professional_type: string; + category: Category[]; +} + +export interface Category { + id: number; + name: string; + icon_name: string; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings { + is_enabled?: boolean; + bitcoin_handle?: string; + ethereum_handle?: string; +} + +export interface Verification { + verified: boolean; +} + +export interface CustomBannerMedia { + media_info: MediaInfo3; +} + +export interface MediaInfo3 { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect3; +} + +export interface SalientRect3 { + left: number; + top: number; + width: number; + height: number; +} + +export interface CustomBannerMediaResults { + result: Result3; +} + +export interface Result3 { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo4; + __typename: string; +} + +export interface MediaInfo4 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect4; + color_info: ColorInfo; +} + +export interface SalientRect4 { + height: number; + left: number; + top: number; + width: number; +} + +export interface ColorInfo { + palette: Palette[]; +} + +export interface Palette { + percentage: number; + rgb: Rgb; +} + +export interface Rgb { + blue: number; + green: number; + red: number; +} + +export interface ClientEventInfo { + component: string; + element: string; + details: Details; +} + +export interface Details { + timelinesDetails: TimelinesDetails; +} + +export interface TimelinesDetails { + injectionType: string; + controllerData?: string; +} + +export interface Header { + displayType: string; + text: string; + sticky: boolean; +} + +export interface Footer { + displayType: string; + text: string; + landingUrl: LandingUrl; +} + +export interface LandingUrl { + url: string; + urlType: string; +} + +export interface ClientEventInfo2 { + component: string; + details: Details2; +} + +export interface Details2 { + timelinesDetails: TimelinesDetails2; +} + +export interface TimelinesDetails2 { + injectionType: string; + controllerData?: string; +} + +export interface Metadata { + scribeConfig: ScribeConfig; +} + +export interface ScribeConfig { + page: string; +} From e31d474a102bcc37231a8437f9f62851319e5e86 Mon Sep 17 00:00:00 2001 From: David Schwertfeger Date: Wed, 6 Aug 2025 21:12:08 +0200 Subject: [PATCH 073/119] Update README.md Fixed a small typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 772b6b82..02b89ab4 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ A new Rettiwt instance can be initialized using the following code snippets: The Rettiwt class has four members: - `dm` member, for accessing resources related to direct messages. -- `list` memeber, for accessing resources related to lists. +- `list` member, for accessing resources related to lists. - `tweet` member, for accessing resources related to tweets. - `user` member, for accessing resources related to users. From 293b5a5bb4101fba105bc196b75ad3c0702839c0 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 26 Aug 2025 12:43:12 +0000 Subject: [PATCH 074/119] Added getter to get the current API key associated with the Rettiwt instance --- package-lock.json | 4 ++-- package.json | 2 +- src/Rettiwt.ts | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83b642e6..2d355252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.5", + "version": "6.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.5", + "version": "6.0.6", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 12548407..0a1f206b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.5", + "version": "6.0.6", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", diff --git a/src/Rettiwt.ts b/src/Rettiwt.ts index 9e4c8548..ed35f7f7 100644 --- a/src/Rettiwt.ts +++ b/src/Rettiwt.ts @@ -70,6 +70,11 @@ export class Rettiwt { this.user = new UserService(this._config); } + /** Get the current API key associated with this instance. */ + public get apiKey(): string | undefined { + return this._config.apiKey; + } + /** Set the API key for the current instance. */ public set apiKey(apiKey: string | undefined) { this._config.apiKey = apiKey; From 9d53142e63b10dc8c99eaeeb87900118d097d516 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 16 Sep 2025 19:46:16 +0530 Subject: [PATCH 075/119] Fixed confusing naming of `isFollowed` and `isFollowing` field of `User` (#769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add following & followed status * Fixed confusing naming for isFollowed and isFollowing field --------- Co-authored-by: 三咲智子 Kevin Deng --- src/models/data/User.ts | 6 ++++++ src/types/data/User.ts | 6 ++++++ src/types/raw/base/User.ts | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/models/data/User.ts b/src/models/data/User.ts index f0029c98..078d7506 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -20,6 +20,8 @@ export class User implements IUser { public followingsCount: number; public fullName: string; public id: string; + public isFollowed: boolean; + public isFollowing: boolean; public isVerified: boolean; public likeCount: number; public location?: string; @@ -39,6 +41,8 @@ export class User implements IUser { this.fullName = user.legacy.name; this.createdAt = new Date(user.legacy.created_at).toISOString(); this.description = user.legacy.description.length ? user.legacy.description : undefined; + this.isFollowed = user.legacy.following ?? false; + this.isFollowing = user.legacy.followed_by ?? false; this.isVerified = user.is_blue_verified; this.likeCount = user.legacy.favourites_count; this.followersCount = user.legacy.followers_count; @@ -168,6 +172,8 @@ export class User implements IUser { followingsCount: this.followingsCount, fullName: this.fullName, id: this.id, + isFollowed: this.isFollowed, + isFollowing: this.isFollowing, isVerified: this.isVerified, likeCount: this.likeCount, location: this.location, diff --git a/src/types/data/User.ts b/src/types/data/User.ts index 1505ff77..de1741ed 100644 --- a/src/types/data/User.ts +++ b/src/types/data/User.ts @@ -22,6 +22,12 @@ export interface IUser { /** The rest id of the user. */ id: string; + /** Whether the user is being followed by the logged-in user. */ + isFollowed: boolean; + + /** Whether the user is following the logged-in user. */ + isFollowing: boolean; + /** Whether the account is verified or not. */ isVerified: boolean; diff --git a/src/types/raw/base/User.ts b/src/types/raw/base/User.ts index ccd9f6c9..81060557 100644 --- a/src/types/raw/base/User.ts +++ b/src/types/raw/base/User.ts @@ -82,7 +82,8 @@ export interface IAffiliateHighlightedMentionResultLegacy { } export interface IUserLegacy { - following: boolean; + followed_by?: boolean; + following?: boolean; can_dm: boolean; can_media_tag: boolean; created_at: string; From c5c7c71639f46935ce49b975a45ddd929ea28b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cavalheiro?= Date: Wed, 8 Oct 2025 07:43:45 -0300 Subject: [PATCH 076/119] Bumped requisite Node to 22.x in README.md (drop 20.x) (#770) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02b89ab4..aef3881c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A CLI tool and an API for fetching data from Twitter for free! ## Prerequisites -- NodeJS 20 +- NodeJS 22 - A working Twitter account (optional) ## Installation From e644a4670b6706c12cd8ed41f48b0481090686dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=81=92=E9=81=93?= <81609753+lihengdao666@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:21:17 +0800 Subject: [PATCH 077/119] Fixed tweet transform error (#776) --- src/models/data/Tweet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index a5d7023c..5a263d46 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -53,14 +53,14 @@ export class Tweet implements ITweet { this.entities = new TweetEntities(tweet.legacy.entities); this.media = tweet.legacy.extended_entities?.media?.map((media) => new TweetMedia(media)); this.quoted = this._getQuotedTweet(tweet); - this.fullText = tweet.note_tweet ? tweet.note_tweet.note_tweet_results.result.text : tweet.legacy.full_text; + this.fullText = tweet.note_tweet?.note_tweet_results?.result?.text ? tweet.note_tweet.note_tweet_results.result.text : tweet.legacy.full_text; this.replyTo = tweet.legacy.in_reply_to_status_id_str; this.lang = tweet.legacy.lang; this.quoteCount = tweet.legacy.quote_count; this.replyCount = tweet.legacy.reply_count; this.retweetCount = tweet.legacy.retweet_count; this.likeCount = tweet.legacy.favorite_count; - this.viewCount = tweet.views.count ? parseInt(tweet.views.count) : 0; + this.viewCount = tweet.views?.count ? parseInt(tweet.views.count) : 0; this.bookmarkCount = tweet.legacy.bookmark_count; this.retweetedTweet = this._getRetweetedTweet(tweet); this.url = `https://x.com/${this.tweetBy.userName}/status/${this.id}`; From 49919f8f08cf5a37abd6130566c606ebed902fa1 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 28 Oct 2025 13:27:17 +0530 Subject: [PATCH 078/119] Updated dependencies --- .gitignore | 4 +++- .nvmrc | 1 + package-lock.json | 37 +++++++++++++++++++++++++------------ package.json | 5 ++--- 4 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 .nvmrc diff --git a/.gitignore b/.gitignore index 0daf12da..f017fc33 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,6 @@ test # Documentation docs -.idea \ No newline at end of file +.idea + +src/debug.ts \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..f5b3ef39 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.21.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fe3bd938..92140d8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "typescript": "^5.7.3" }, "engines": { - "node": "^22.13.1" + "node": "^22.21.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -214,19 +214,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gerrit0/mini-shiki": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", @@ -953,13 +966,13 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2167,9 +2180,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", diff --git a/package.json b/package.json index 12477cc2..62285879 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "format:check": "prettier --check .", "lint": "eslint --max-warnings 0 --fix .", "lint:check": "eslint --max-warnings 0 .", - "docs": "typedoc --excludePrivate --excludeProtected --excludeInternal src/index.ts", - "debug": "nodemon ./dist/index.js --inspect=0.0.0.0:9229" + "docs": "typedoc --excludePrivate --excludeProtected --excludeInternal src/index.ts" }, "repository": { "type": "git", @@ -31,7 +30,7 @@ }, "homepage": "https://rishikant181.github.io/Rettiwt-API/", "engines": { - "node": "^22.13.1" + "node": "^22.21.0" }, "devDependencies": { "@types/cookiejar": "^2.1.5", From e1a531e81739d490580f184afb4f7a71d5c13366 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 30 Oct 2025 22:22:12 +0530 Subject: [PATCH 079/119] Fixed an issue where tweet failed to post due to incorrect user agent (#778) --- package-lock.json | 35 ++++++++++++++++++--------- src/models/RettiwtConfig.ts | 3 +-- src/services/public/FetcherService.ts | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d355252..bfc4e5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -210,19 +210,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gerrit0/mini-shiki": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", @@ -949,13 +962,13 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2151,9 +2164,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index ee67178a..dee949c1 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -18,8 +18,7 @@ const DefaultHeaders = { 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'no-cache', Referer: 'https://x.com', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0', 'X-Twitter-Active-User': 'yes', 'X-Twitter-Client-Language': 'en', diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 52833d26..d3c54fb3 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -233,8 +233,8 @@ export class FetcherService { try { // Getting and appending transaction information config.headers = { - ...config.headers, ...(await this._getTransactionHeader(config.method ?? '', config.url ?? '')), + ...config.headers, }; // Introducing a delay From e1cdeb9dd36641f3d29fc362473f6d973ded4c0d Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 30 Oct 2025 16:52:39 +0000 Subject: [PATCH 080/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfc4e5d0..a9bc63db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.6", + "version": "6.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.6", + "version": "6.0.7", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 0a1f206b..27ca3c86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.6", + "version": "6.0.7", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 537801d21ce15cf8ed030c41fdcef5ab9eb3ca30 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 5 Nov 2025 15:43:21 +0000 Subject: [PATCH 081/119] Updated docs --- src/requests/DirectMessage.ts | 22 +++++++++++---------- src/services/public/DirectMessageService.ts | 8 ++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/requests/DirectMessage.ts b/src/requests/DirectMessage.ts index ab3fbe0c..b4398f59 100644 --- a/src/requests/DirectMessage.ts +++ b/src/requests/DirectMessage.ts @@ -5,9 +5,9 @@ import { AxiosRequestConfig } from 'axios'; /** * Common parameter sets for DM requests */ -// eslint-disable-next-line @typescript-eslint/naming-convention -const DM_BASE_PARAMS = { +const BaseDMParams = { /* eslint-disable @typescript-eslint/naming-convention */ + nsfw_filtering_enabled: false, filter_low_quality: true, include_quality: 'all', @@ -29,12 +29,13 @@ const DM_BASE_PARAMS = { include_ext_edit_control: true, include_ext_business_affiliations_label: true, ext: 'mediaColor%2CaltText%2CbusinessAffiliationsLabel%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + /* eslint-enable @typescript-eslint/naming-convention */ }; -// eslint-disable-next-line @typescript-eslint/naming-convention -const DM_USER_INCLUDE_PARAMS = { +const DMUserIncludeParams = { /* eslint-disable @typescript-eslint/naming-convention */ + include_profile_interstitial_type: 1, include_blocking: 1, include_blocked_by: 1, @@ -47,6 +48,7 @@ const DM_USER_INCLUDE_PARAMS = { include_ext_verified_type: 1, include_ext_profile_image_shape: 1, skip_status: 1, + /* eslint-enable @typescript-eslint/naming-convention */ }; @@ -68,8 +70,8 @@ export class DMRequests { method: 'get', url: `https://x.com/i/api/1.1/dm/conversation/${conversationId}.json`, params: { - ...DM_BASE_PARAMS, - ...DM_USER_INCLUDE_PARAMS, + ...BaseDMParams, + ...DMUserIncludeParams, /* eslint-disable @typescript-eslint/naming-convention */ max_id: maxId, context: context, @@ -120,8 +122,8 @@ export class DMRequests { method: 'get', url: 'https://x.com/i/api/1.1/dm/inbox_initial_state.json', params: { - ...DM_BASE_PARAMS, - ...DM_USER_INCLUDE_PARAMS, + ...BaseDMParams, + ...DMUserIncludeParams, /* eslint-disable @typescript-eslint/naming-convention */ dm_users: true, include_ext_parody_commentary_fan_label: true, @@ -140,8 +142,8 @@ export class DMRequests { method: 'get', url: 'https://x.com/i/api/1.1/dm/inbox_timeline/trusted.json', params: { - ...DM_BASE_PARAMS, - ...DM_USER_INCLUDE_PARAMS, + ...BaseDMParams, + ...DMUserIncludeParams, /* eslint-disable @typescript-eslint/naming-convention */ max_id: maxId, dm_users: false, diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts index 23461610..cd4a133c 100644 --- a/src/services/public/DirectMessageService.ts +++ b/src/services/public/DirectMessageService.ts @@ -25,11 +25,11 @@ export class DirectMessageService extends FetcherService { } /** - * Get the full conversation history for a specific conversation. + * Get the full conversation history for a specific conversation, ordered recent to oldest. * Use this to load complete message history for a conversation identified from the inbox. * * @param conversationId - The ID of the conversation (e.g., "394028042-1712730991884689408"). - * @param cursor - The cursor for pagination. + * @param cursor - The cursor for pagination. Is equal to the ID of the last message from previous batch. * * @returns The conversation with full message history, or undefined if not found. * @@ -103,9 +103,9 @@ export class DirectMessageService extends FetcherService { } /** - * Get your inbox. + * Get your inbox, ordered recent to oldest. * - * @param cursor - The cursor to the inbox items to fetch. If not provided, intial inbox with most recent conversations is fetched. + * @param cursor - The cursor to the inbox items to fetch. Is equal to the ID of the last inbox conversation. * * @returns The required inbox. Returns initial inbox if no cursor is provided. * From 65c08b7ff1d603cf0d7bf55d0f6972b30747672e Mon Sep 17 00:00:00 2001 From: HadesZ Date: Sun, 9 Nov 2025 15:33:43 +0800 Subject: [PATCH 082/119] Appending '@' to all-numeric username while getting user details distiguishes it from a numeric ID (#787) --- src/models/data/Tweet.ts | 4 +++- src/services/public/UserService.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index 5a263d46..ae0af7b6 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -53,7 +53,9 @@ export class Tweet implements ITweet { this.entities = new TweetEntities(tweet.legacy.entities); this.media = tweet.legacy.extended_entities?.media?.map((media) => new TweetMedia(media)); this.quoted = this._getQuotedTweet(tweet); - this.fullText = tweet.note_tweet?.note_tweet_results?.result?.text ? tweet.note_tweet.note_tweet_results.result.text : tweet.legacy.full_text; + this.fullText = tweet.note_tweet?.note_tweet_results?.result?.text + ? tweet.note_tweet.note_tweet_results.result.text + : tweet.legacy.full_text; this.replyTo = tweet.legacy.in_reply_to_status_id_str; this.lang = tweet.legacy.lang; this.quoteCount = tweet.legacy.quote_count; diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 13f1a133..59eff4da 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -228,8 +228,8 @@ export class UserService extends FetcherService { * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * - * // Fetching the details of the User with username 'user1' - * rettiwt.user.details('user1') + * // Fetching the details of the User with username 'user1' or '@user1' + * rettiwt.user.details('user1') // or @user1 * .then(res => { * console.log(res); * }) @@ -306,6 +306,9 @@ export class UserService extends FetcherService { // If username is given if (id && isNaN(Number(id))) { resource = ResourceType.USER_DETAILS_BY_USERNAME; + if (id?.startsWith('@')) { + id = id.slice(1); + } } // If id is given (or not, for self details) else { From 42f0717e38d75680f792fff63afe48cff29b9e74 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 14 Nov 2025 18:19:09 +0000 Subject: [PATCH 083/119] Updated .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0daf12da..086bf41a 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,7 @@ test # Documentation docs -.idea \ No newline at end of file +.idea + +# Debugging +src/debug.ts \ No newline at end of file From 2bf2b090c1df3b9f283db666c4103817fea2a5ca Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Sat, 15 Nov 2025 06:42:44 +0000 Subject: [PATCH 084/119] Transaction ID generation now respects custom headers and proxy (#790) --- src/services/public/FetcherService.ts | 86 ++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index d3c54fb3..fc057eaf 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,6 +1,7 @@ import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; -import { ClientTransaction, handleXMigration } from 'x-client-transaction-id-glacier'; +import { JSDOM } from 'jsdom'; +import { ClientTransaction } from 'x-client-transaction-id-glacier'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; import { Requests } from '../../collections/Requests'; @@ -103,7 +104,7 @@ export class FetcherService { */ private async _getTransactionHeader(method: string, url: string): Promise { // Get the X homepage HTML document (using utility function) - const document = await handleXMigration(); + const document = await this._handleXMigration(); // Create and initialize ClientTransaction instance const transaction = await ClientTransaction.create(document); @@ -121,6 +122,87 @@ export class FetcherService { }; } + private async _handleXMigration(): Promise { + // Fetch X.com homepage + const homePageResponse = await axios.get('https://x.com', { + headers: this.config.headers, + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + // Parse HTML using linkedom + let dom = new JSDOM(homePageResponse.data); + let document = dom.window.document; + + // Check for migration redirection links + const migrationRedirectionRegex = new RegExp( + '(http(?:s)?://(?:www\\.)?(twitter|x){1}\\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\\-_]+)+', + 'i', + ); + + const metaRefresh = document.querySelector("meta[http-equiv='refresh']"); + const metaContent = metaRefresh ? metaRefresh.getAttribute('content') || '' : ''; + + const migrationRedirectionUrl = + migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(homePageResponse.data); + + if (migrationRedirectionUrl) { + // Follow redirection URL + const redirectResponse = await axios.get(migrationRedirectionUrl[0], { + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + dom = new JSDOM(redirectResponse.data); + document = dom.window.document; + } + + // Handle migration form if present + const migrationForm = + document.querySelector("form[name='f']") || + document.querySelector("form[action='https://x.com/x/migrate']"); + + if (migrationForm) { + const url = migrationForm.getAttribute('action') || 'https://x.com/x/migrate'; + const method = migrationForm.getAttribute('method') || 'POST'; + + // Collect form input fields + const requestPayload = new FormData(); + + const inputFields = migrationForm.querySelectorAll('input'); + for (const element of Array.from(inputFields)) { + const name = element.getAttribute('name'); + const value = element.getAttribute('value'); + if (name && value) { + requestPayload.append(name, value); + } + } + + // Submit form using POST request + const formResponse = await axios.request({ + method: method, + url: url, + data: requestPayload, + headers: { + /* eslint-disable @typescript-eslint/naming-convention */ + + 'Content-Type': 'multipart/form-data', + ...this.config.headers, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + dom = new JSDOM(formResponse.data); + document = dom.window.document; + } + + // Return final DOM document + return document; + } + /** * Validates the given args against the given resource. * From 940c7a8864b4b4670886268b8179d41091f20334 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Sat, 15 Nov 2025 06:44:54 +0000 Subject: [PATCH 085/119] Fixed dependencies --- package-lock.json | 568 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 564 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9bc63db..6a0e955c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.7", + "version": "6.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.7", + "version": "6.0.8", "license": "ISC", "dependencies": { "axios": "^1.8.4", @@ -14,6 +14,7 @@ "commander": "^11.1.0", "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", + "jsdom": "^27.2.0", "node-html-parser": "^7.0.1", "x-client-transaction-id-glacier": "^1.0.0" }, @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/cookiejar": "^2.1.5", + "@types/jsdom": "^27.0.0", "@types/node": "^22.13.1", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", @@ -37,6 +39,173 @@ "node": "^22.13.1" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -456,6 +625,44 @@ "@types/unist": "*" } }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -480,6 +687,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -979,6 +1193,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1215,6 +1438,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -1233,6 +1469,33 @@ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "license": "MIT" }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1304,6 +1567,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2451,6 +2720,18 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -2488,6 +2769,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2501,6 +2795,18 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2819,6 +3125,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -2986,9 +3298,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2998,6 +3310,45 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3102,6 +3453,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -3136,6 +3496,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -3490,6 +3856,30 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3583,7 +3973,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3681,7 +4070,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3808,6 +4196,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -3982,6 +4388,15 @@ "node": ">=10" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -4104,6 +4519,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4127,6 +4566,30 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4337,6 +4800,61 @@ "punycode": "^2.1.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4452,6 +4970,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/x-client-transaction-id-glacier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/x-client-transaction-id-glacier/-/x-client-transaction-id-glacier-1.0.0.tgz", @@ -4461,6 +5000,21 @@ "linkedom": "^0.18.9" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", diff --git a/package.json b/package.json index 27ca3c86..3fc3a273 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.7", + "version": "6.0.8", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", @@ -33,6 +33,7 @@ }, "devDependencies": { "@types/cookiejar": "^2.1.5", + "@types/jsdom": "^27.0.0", "@types/node": "^22.13.1", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", @@ -50,6 +51,7 @@ "commander": "^11.1.0", "cookiejar": "^2.1.4", "https-proxy-agent": "^7.0.6", + "jsdom": "^27.2.0", "node-html-parser": "^7.0.1", "x-client-transaction-id-glacier": "^1.0.0" } From ee9427c653917476ad0ea0a250d57919e19ab8e7 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 19 Nov 2025 15:32:15 +0000 Subject: [PATCH 086/119] Fixed typo in documentation --- src/services/public/UserService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 59eff4da..46b69528 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -598,7 +598,7 @@ export class UserService extends FetcherService { * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * * // Fetching the first 100 Lists of the logged in User - * rettiwt.user.likes() + * rettiwt.user.lists() * .then(res => { * console.log(res); * }) From e9c6bc84174cd0b4bc04fe944104aaafcda9e351 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 19 Nov 2025 15:38:48 +0000 Subject: [PATCH 087/119] Impression count fields are now optional in Tweet (#793) --- src/models/data/Tweet.ts | 14 +++++++------- src/types/data/Tweet.ts | 12 ++++++------ src/types/raw/base/Tweet.ts | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index ae0af7b6..c75a5660 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -22,24 +22,24 @@ export class Tweet implements ITweet { /** The raw tweet details. */ private readonly _raw: IRawTweet; - public bookmarkCount: number; + public bookmarkCount?: number; public conversationId: string; public createdAt: string; public entities: TweetEntities; public fullText: string; public id: string; public lang: string; - public likeCount: number; + public likeCount?: number; public media?: TweetMedia[]; - public quoteCount: number; + public quoteCount?: number; public quoted?: Tweet; - public replyCount: number; + public replyCount?: number; public replyTo?: string; - public retweetCount: number; + public retweetCount?: number; public retweetedTweet?: Tweet; public tweetBy: User; public url: string; - public viewCount: number; + public viewCount?: number; /** * @param tweet - The raw tweet details. @@ -62,7 +62,7 @@ export class Tweet implements ITweet { this.replyCount = tweet.legacy.reply_count; this.retweetCount = tweet.legacy.retweet_count; this.likeCount = tweet.legacy.favorite_count; - this.viewCount = tweet.views?.count ? parseInt(tweet.views.count) : 0; + this.viewCount = tweet.views?.count ? parseInt(tweet.views.count) : undefined; this.bookmarkCount = tweet.legacy.bookmark_count; this.retweetedTweet = this._getRetweetedTweet(tweet); this.url = `https://x.com/${this.tweetBy.userName}/status/${this.id}`; diff --git a/src/types/data/Tweet.ts b/src/types/data/Tweet.ts index 47081a39..1cb1add6 100644 --- a/src/types/data/Tweet.ts +++ b/src/types/data/Tweet.ts @@ -9,7 +9,7 @@ import { IUser } from './User'; */ export interface ITweet { /** The number of bookmarks of a tweet. */ - bookmarkCount: number; + bookmarkCount?: number; /** The ID of tweet which started the current conversation. */ conversationId: string; @@ -30,25 +30,25 @@ export interface ITweet { lang: string; /** The number of likes of the tweet. */ - likeCount: number; + likeCount?: number; /** The urls of the media contents of the tweet (if any). */ media?: ITweetMedia[]; /** The number of quotes of the tweet. */ - quoteCount: number; + quoteCount?: number; /** The tweet which is quoted in the tweet. */ quoted?: ITweet; /** The number of replies to the tweet. */ - replyCount: number; + replyCount?: number; /** The rest id of the tweet to which the tweet is a reply. */ replyTo?: string; /** The number of retweets of the tweet. */ - retweetCount: number; + retweetCount?: number; /** The tweet which is retweeted in this tweet (if any). */ retweetedTweet?: ITweet; @@ -60,7 +60,7 @@ export interface ITweet { url: string; /** The number of views of a tweet. */ - viewCount: number; + viewCount?: number; } /** diff --git a/src/types/raw/base/Tweet.ts b/src/types/raw/base/Tweet.ts index 10bc5960..202190e3 100644 --- a/src/types/raw/base/Tweet.ts +++ b/src/types/raw/base/Tweet.ts @@ -17,7 +17,7 @@ export interface ITweet { edit_control: ITweetEditControl; edit_perspective: ITweetEditPerspective; is_translatable: boolean; - views: ITweetViews; + views?: ITweetViews; source: string; quoted_status_result: IDataResult; note_tweet: ITweetNote; @@ -78,14 +78,14 @@ export interface ITweetNoteMedia { } export interface ITweetLegacy { - bookmark_count: number; + bookmark_count?: number; bookmarked: boolean; created_at: string; conversation_id_str: string; display_text_range: number[]; entities: IEntities; extended_entities: IExtendedEntities; - favorite_count: number; + favorite_count?: number; favorited: boolean; full_text: string; in_reply_to_status_id_str: string; @@ -93,10 +93,10 @@ export interface ITweetLegacy { lang: string; possibly_sensitive: boolean; possibly_sensitive_editable: boolean; - quote_count: number; + quote_count?: number; quoted_status_id_str: string; - reply_count: number; - retweet_count: number; + reply_count?: number; + retweet_count?: number; retweeted: boolean; user_id_str: string; id_str: string; From 953ce0f70dd448f488c2ced81ca9e01e9faae28b Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 19 Nov 2025 15:43:56 +0000 Subject: [PATCH 088/119] Made isFollowed and isFollowing field optional (#794) --- src/models/data/User.ts | 8 ++++---- src/types/data/User.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/models/data/User.ts b/src/models/data/User.ts index 078d7506..4c7591e7 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -20,8 +20,8 @@ export class User implements IUser { public followingsCount: number; public fullName: string; public id: string; - public isFollowed: boolean; - public isFollowing: boolean; + public isFollowed?: boolean; + public isFollowing?: boolean; public isVerified: boolean; public likeCount: number; public location?: string; @@ -41,8 +41,8 @@ export class User implements IUser { this.fullName = user.legacy.name; this.createdAt = new Date(user.legacy.created_at).toISOString(); this.description = user.legacy.description.length ? user.legacy.description : undefined; - this.isFollowed = user.legacy.following ?? false; - this.isFollowing = user.legacy.followed_by ?? false; + this.isFollowed = user.legacy.following; + this.isFollowing = user.legacy.followed_by; this.isVerified = user.is_blue_verified; this.likeCount = user.legacy.favourites_count; this.followersCount = user.legacy.followers_count; diff --git a/src/types/data/User.ts b/src/types/data/User.ts index de1741ed..85d97c3a 100644 --- a/src/types/data/User.ts +++ b/src/types/data/User.ts @@ -22,11 +22,11 @@ export interface IUser { /** The rest id of the user. */ id: string; - /** Whether the user is being followed by the logged-in user. */ - isFollowed: boolean; + /** Whether the user is being followed by the logged-in user. Available only when logged in. */ + isFollowed?: boolean; - /** Whether the user is following the logged-in user. */ - isFollowing: boolean; + /** Whether the user is following the logged-in user. Available only when logged in. */ + isFollowing?: boolean; /** Whether the account is verified or not. */ isVerified: boolean; From eb064a1dccffd85930e85c267fd4765c38ea4916 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 19 Nov 2025 15:45:53 +0000 Subject: [PATCH 089/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c980807c..a8d0f50c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.0.8", + "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.0.8", + "version": "6.1.0", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 8a916cc1..a28e0288 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.0.8", + "version": "6.1.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 8742fb8d0ba8aee9099bfbec9b5e8fb3e03fd38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Sun, 23 Nov 2025 01:01:59 +0000 Subject: [PATCH 090/119] Fixed tweet.post() returning undefined on Error 226 (#796) --- src/services/public/FetcherService.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 55fbde8f..a469b197 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,4 +1,4 @@ -import axios, { isAxiosError } from 'axios'; +import axios, { AxiosError, isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; import { JSDOM } from 'jsdom'; import { ClientTransaction } from 'x-client-transaction-id-glacier'; @@ -11,11 +11,13 @@ import { ResourceType } from '../../enums/Resource'; import { FetchArgs } from '../../models/args/FetchArgs'; import { PostArgs } from '../../models/args/PostArgs'; import { AuthCredential } from '../../models/auth/AuthCredential'; +import { TwitterError } from '../../models/errors/TwitterError'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IFetchArgs } from '../../types/args/FetchArgs'; import { IPostArgs } from '../../types/args/PostArgs'; import { ITransactionHeader } from '../../types/auth/TransactionHeader'; import { IErrorHandler } from '../../types/ErrorHandler'; +import { IErrorData } from '../../types/raw/base/Error'; import { AuthService } from '../internal/AuthService'; import { ErrorService } from '../internal/ErrorService'; @@ -322,8 +324,27 @@ export class FetcherService { // Introducing a delay await this._wait(); + // Getting the response body + const responseData = (await axios(config)).data; + + // Check for Twitter API errors in response body + // Type guard to check if response contains errors + const potentialErrorResponse = responseData as unknown as Partial; + if (potentialErrorResponse.errors && Array.isArray(potentialErrorResponse.errors)) { + // Throw TwitterError using existing error class + const axiosError = { + response: { + data: { errors: potentialErrorResponse.errors }, + status: 200, + }, + message: potentialErrorResponse.errors[0]?.message ?? 'Twitter API Error', + status: 200, + } as AxiosError; + throw new TwitterError(axiosError); + } + // Returning the reponse body - return (await axios(config)).data; + return responseData; } catch (err) { // If it's an error 404, retry if (isAxiosError(err) && err.status === 404) { From 7a9456707d771f5603c2c5c3859d14d29ffdb3b3 Mon Sep 17 00:00:00 2001 From: karasungur Date: Tue, 25 Nov 2025 12:46:08 +0300 Subject: [PATCH 091/119] Add Comprehensive User Profile Update Functionality This update introduces the ability for authenticated users to fully manage their profile details directly via the API and CLI. Key Features: - Update Display Name, Bio, Location, and Website URL. - New CLI command: 'rettiwt user update-profile'. - Built-in validation for character limits and data integrity. - Secure implementation restricted to authenticated users only. --- README.md | 2 + src/collections/Extractors.ts | 2 + src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/User.ts | 21 ++++++++ src/enums/Resource.ts | 1 + src/index.ts | 3 ++ src/models/args/PostArgs.ts | 6 +++ src/models/args/ProfileArgs.ts | 68 ++++++++++++++++++++++++ src/requests/User.ts | 17 ++++++ src/services/public/UserService.ts | 68 ++++++++++++++++++++++++ src/types/args/PostArgs.ts | 10 ++++ src/types/args/ProfileArgs.ts | 33 ++++++++++++ src/types/raw/user/ProfileUpdate.ts | 80 +++++++++++++++++++++++++++++ 14 files changed, 313 insertions(+) create mode 100644 src/models/args/ProfileArgs.ts create mode 100644 src/types/args/ProfileArgs.ts create mode 100644 src/types/raw/user/ProfileUpdate.ts diff --git a/README.md b/README.md index aef3881c..b19746fe 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Subscriptions - User Timeline - User Unfollow + - User Profile Update By default, Rettiwt-API uses 'guest' authentication. If however, access to the full set of resources is required, 'user' authentication can be used. This is done by using the cookies associated with your Twitter/X account, and encoding them into an `API_KEY` for convenience. The said `API_KEY` can be obtained by using a browser extension, as follows: @@ -498,6 +499,7 @@ So far, the following operations are supported: - [Getting the replies timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#replies) - [Getting the tweet timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#timeline) - [Unfollowing a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#unfollow) +- [Updating the profile of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#updateProfile) ## CLI Usage diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index becd90f1..ef5b0239 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -46,6 +46,7 @@ import { IUserLikesResponse } from '../types/raw/user/Likes'; import { IUserListsResponse } from '../types/raw/user/Lists'; import { IUserMediaResponse } from '../types/raw/user/Media'; import { IUserNotificationsResponse } from '../types/raw/user/Notifications'; +import { IUserProfileUpdateResponse } from '../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../types/raw/user/Recommended'; import { IUserSubscriptionsResponse } from '../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../types/raw/user/Tweets'; @@ -139,6 +140,7 @@ export const Extractors = { USER_TIMELINE_AND_REPLIES: (response: IUserTweetsAndRepliesResponse): CursoredData => new CursoredData(response, BaseType.TWEET), USER_UNFOLLOW: (response: IUserUnfollowResponse): boolean => (response?.id ? true : false), + USER_PROFILE_UPDATE: (response: IUserProfileUpdateResponse): boolean => (response?.name ? true : false), /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 77657ebe..3b53c688 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -74,4 +74,5 @@ export const PostResourcesGroup = [ ResourceType.TWEET_UNSCHEDULE, ResourceType.USER_FOLLOW, ResourceType.USER_UNFOLLOW, + ResourceType.USER_PROFILE_UPDATE, ]; diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index b3439db6..48d4e09a 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -80,6 +80,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | USER_TIMELINE: (args: IFetchArgs) => UserRequests.tweets(args.id!, args.count, args.cursor), USER_TIMELINE_AND_REPLIES: (args: IFetchArgs) => UserRequests.tweetsAndReplies(args.id!, args.count, args.cursor), USER_UNFOLLOW: (args: IPostArgs) => UserRequests.unfollow(args.id!), + USER_PROFILE_UPDATE: (args: IPostArgs) => UserRequests.updateProfile(args.profileOptions!), /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/commands/User.ts b/src/commands/User.ts index dae5c5b6..7954286c 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -274,6 +274,27 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Update Profile + user.command('update-profile') + .description('Update your profile information') + .option('-n, --name ', 'Display name (max 50 characters)') + .option('-u, --url ', 'Profile URL') + .option('-l, --location ', 'Location (max 30 characters)') + .option('-b, --bio ', 'Bio/description (max 160 characters)') + .action(async (options?: UserProfileUpdateOptions) => { + try { + const result = await rettiwt.user.updateProfile({ + name: options?.name, + url: options?.url, + location: options?.location, + description: options?.bio, + }); + output(result); + } catch (error) { + output(error); + } + }); + return user; } diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index c8894fc3..17a88168 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -62,4 +62,5 @@ export enum ResourceType { USER_TIMELINE = 'USER_TIMELINE', USER_TIMELINE_AND_REPLIES = 'USER_TIMELINE_AND_REPLIES', USER_UNFOLLOW = 'USER_UNFOLLOW', + USER_PROFILE_UPDATE = 'USER_PROFILE_UPDATE', } diff --git a/src/index.ts b/src/index.ts index 2881aed2..3889dfc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ export * from './enums/Tweet'; // MODELS export * from './models/args/FetchArgs'; export * from './models/args/PostArgs'; +export * from './models/args/ProfileArgs'; export * from './models/data/Conversation'; export * from './models/data/CursoredData'; export * from './models/data/DirectMessage'; @@ -45,6 +46,7 @@ export * from './services/public/UserService'; // TYPES export * from './types/args/FetchArgs'; export * from './types/args/PostArgs'; +export * from './types/args/ProfileArgs'; export * from './types/data/Conversation'; export * from './types/data/CursoredData'; export * from './types/data/DirectMessage'; @@ -111,6 +113,7 @@ export { IUserSubscriptionsResponse as IRawUserSubscriptionsResponse } from './t export { IUserTweetsResponse as IRawUserTweetsResponse } from './types/raw/user/Tweets'; export { IUserTweetsAndRepliesResponse as IRawUserTweetsAndRepliesResponse } from './types/raw/user/TweetsAndReplies'; export { IUserUnfollowResponse as IRawUserUnfollowResponse } from './types/raw/user/Unfollow'; +export { IUserProfileUpdateResponse as IRawUserProfileUpdateResponse } from './types/raw/user/ProfileUpdate'; export * from './types/ErrorHandler'; export * from './types/RettiwtConfig'; export { IConversationTimelineResponse as IRawConversationTimelineResponse } from './types/raw/dm/Conversation'; diff --git a/src/models/args/PostArgs.ts b/src/models/args/PostArgs.ts index 436df5d2..76fbc8e3 100644 --- a/src/models/args/PostArgs.ts +++ b/src/models/args/PostArgs.ts @@ -1,12 +1,16 @@ import { INewTweet, INewTweetMedia, IPostArgs, IUploadArgs } from '../../types/args/PostArgs'; +import { ProfileUpdateOptions } from './ProfileArgs'; + /** * Options specifying the data that is to be posted. * * @public */ export class PostArgs implements IPostArgs { + public conversationId?: string; public id?: string; + public profileOptions?: ProfileUpdateOptions; public tweet?: NewTweet; public upload?: UploadArgs; public userId?: string; @@ -20,6 +24,8 @@ export class PostArgs implements IPostArgs { this.tweet = args.tweet ? new NewTweet(args.tweet) : undefined; this.upload = args.upload ? new UploadArgs(args.upload) : undefined; this.userId = args.userId; + this.conversationId = args.conversationId; + this.profileOptions = args.profileOptions ? new ProfileUpdateOptions(args.profileOptions) : undefined; } } diff --git a/src/models/args/ProfileArgs.ts b/src/models/args/ProfileArgs.ts new file mode 100644 index 00000000..0b9520ae --- /dev/null +++ b/src/models/args/ProfileArgs.ts @@ -0,0 +1,68 @@ +import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; + +/** + * Configuration for profile update. + * + * @public + */ +export class ProfileUpdateOptions implements IProfileUpdateOptions { + public description?: string; + public location?: string; + public name?: string; + public url?: string; + + /** + * @param options - The options specifying the profile fields to update. + */ + public constructor(options: IProfileUpdateOptions) { + this.description = options.description; + this.location = options.location; + this.name = options.name; + this.url = options.url; + + // At least one field must be provided + if ( + this.name === undefined && + this.url === undefined && + this.location === undefined && + this.description === undefined + ) { + throw new Error('At least one profile field must be provided'); + } + + // Name validation + if (this.name !== undefined) { + if (this.name.trim().length === 0) { + throw new Error('Name cannot be empty'); + } + if (this.name.length > 50) { + throw new Error('Name cannot exceed 50 characters'); + } + } + + // URL validation (minimal - just check if not empty when provided) + if (this.url !== undefined && this.url.trim().length === 0) { + throw new Error('URL cannot be empty'); + } + + // Location validation + if (this.location !== undefined) { + if (this.location.trim().length === 0) { + throw new Error('Location cannot be empty'); + } + if (this.location.length > 30) { + throw new Error('Location cannot exceed 30 characters'); + } + } + + // Description validation + if (this.description !== undefined) { + if (this.description.trim().length === 0) { + throw new Error('Description cannot be empty'); + } + if (this.description.length > 160) { + throw new Error('Description cannot exceed 160 characters'); + } + } + } +} diff --git a/src/requests/User.ts b/src/requests/User.ts index 438ed40c..a23a361c 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -3,6 +3,7 @@ import qs from 'querystring'; import { AxiosRequestConfig } from 'axios'; import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics'; +import { IProfileUpdateOptions } from '../types/args/ProfileArgs'; /** * Collection of requests related to users. @@ -963,4 +964,20 @@ export class UserRequests { }), }; } + + /** + * @param options - The profile update options. + */ + public static updateProfile(options: IProfileUpdateOptions): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/1.1/account/update_profile.json', + data: qs.stringify({ + ...(options.name && { name: options.name }), + ...(options.url && { url: options.url }), + ...(options.location && { location: options.location }), + ...(options.description && { description: options.description }), + }), + }; + } } diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 46b69528..04303612 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -22,7 +22,10 @@ import { IUserLikesResponse } from '../../types/raw/user/Likes'; import { IUserListsResponse } from '../../types/raw/user/Lists'; import { IUserMediaResponse } from '../../types/raw/user/Media'; import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; +import { IUserProfileUpdateResponse } from '../../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../../types/raw/user/Recommended'; +import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; +import { ProfileUpdateOptions } from '../../models/args/ProfileArgs'; import { IUserSubscriptionsResponse } from '../../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../../types/raw/user/TweetsAndReplies'; @@ -956,4 +959,69 @@ export class UserService extends FetcherService { return data; } + + /** + * Update the logged in user's profile. + * + * @param options - The profile update options. + * + * @returns Whether the profile update was successful or not. + * + * @example + * + * #### Updating only the display name + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Updating the display name of the logged in user + * rettiwt.user.updateProfile({ name: 'New Display Name' }) + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + * + * @example + * + * #### Updating multiple profile fields + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Updating multiple profile fields + * rettiwt.user.updateProfile({ + * name: 'New Display Name', + * location: 'Istanbul', + * description: 'Hello world!', + * url: 'https://example.com' + * }) + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async updateProfile(options: IProfileUpdateOptions): Promise { + const resource = ResourceType.USER_PROFILE_UPDATE; + + // Validating the options + const validatedOptions = new ProfileUpdateOptions(options); + + // Updating the profile + const response = await this.request(resource, { profileOptions: validatedOptions }); + + // Deserializing the response + const data = Extractors[resource](response) ?? false; + + return data; + } } diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index bc33053a..26bdf31e 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -1,3 +1,5 @@ +import { IProfileUpdateOptions } from './ProfileArgs'; + /** * Options specifying the data that is to be posted. * @@ -57,6 +59,14 @@ export interface IPostArgs { * Required only when deleting a conversation using {@link ResourceType.DM_DELETE_CONVERSATION} */ conversationId?: string; + + /** + * Profile update options. + * + * @remarks + * Required only when updating user profile using {@link ResourceType.USER_PROFILE_UPDATE} + */ + profileOptions?: IProfileUpdateOptions; } /** diff --git a/src/types/args/ProfileArgs.ts b/src/types/args/ProfileArgs.ts new file mode 100644 index 00000000..25212941 --- /dev/null +++ b/src/types/args/ProfileArgs.ts @@ -0,0 +1,33 @@ +/** + * Profile update options. + * + * @public + */ +export interface IProfileUpdateOptions { + /** + * Bio/description of the user (max 160 characters). + */ + description?: string; + + /** + * Location of the user (max 30 characters). + */ + location?: string; + + /** + * Display name (max 50 characters). + * + * @remarks + * The name field represents the user's display name shown on their profile. + * This is different from the username (screen_name/handle). + */ + name?: string; + + /** + * URL associated with the profile. + * + * @remarks + * Will be prepended with http:// if not present. + */ + url?: string; +} diff --git a/src/types/raw/user/ProfileUpdate.ts b/src/types/raw/user/ProfileUpdate.ts new file mode 100644 index 00000000..11f20d7c --- /dev/null +++ b/src/types/raw/user/ProfileUpdate.ts @@ -0,0 +1,80 @@ +/* eslint-disable */ + +/** + * The raw data received when updating user profile. + * + * @public + */ +export interface IUserProfileUpdateResponse { + id: number; + id_str: string; + name: string; + screen_name: string; + location: string; + description: string; + url: string | null; + entities: Entities; + protected: boolean; + followers_count: number; + fast_followers_count: number; + normal_followers_count: number; + friends_count: number; + listed_count: number; + created_at: string; + favourites_count: number; + utc_offset: any; + time_zone: any; + geo_enabled: boolean; + verified: boolean; + statuses_count: number; + media_count: number; + lang: any; + contributors_enabled: boolean; + is_translator: boolean; + is_translation_enabled: boolean; + profile_background_color: string; + profile_background_image_url: string; + profile_background_image_url_https: string; + profile_background_tile: boolean; + profile_image_url: string; + profile_image_url_https: string; + profile_banner_url: string; + profile_link_color: string; + profile_sidebar_border_color: string; + profile_sidebar_fill_color: string; + profile_text_color: string; + profile_use_background_image: boolean; + has_extended_profile: boolean; + default_profile: boolean; + default_profile_image: boolean; + pinned_tweet_ids: number[]; + pinned_tweet_ids_str: string[]; + has_custom_timelines: boolean; + can_media_tag: boolean; + advertiser_account_type: string; + advertiser_account_service_levels: any[]; + business_profile_state: string; + translator_type: string; + withheld_in_countries: any[]; + require_some_consent: boolean; +} + +interface Entities { + description: Description; + url?: Url; +} + +interface Description { + urls: any[]; +} + +interface Url { + urls: UrlDetail[]; +} + +interface UrlDetail { + url: string; + expanded_url: string; + display_url: string; + indices: number[]; +} From 6de2ec89dfd3f950da069e20d83a5201a3795fb8 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Thu, 27 Nov 2025 12:50:36 +0000 Subject: [PATCH 092/119] Fixed compilation errors --- src/commands/User.ts | 14 ++++++++++++-- src/services/public/UserService.ts | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commands/User.ts b/src/commands/User.ts index 7954286c..0f900f48 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -280,14 +280,14 @@ function createUserCommand(rettiwt: Rettiwt): Command { .option('-n, --name ', 'Display name (max 50 characters)') .option('-u, --url ', 'Profile URL') .option('-l, --location ', 'Location (max 30 characters)') - .option('-b, --bio ', 'Bio/description (max 160 characters)') + .option('-d, --description ', 'Description/bio (max 160 characters)') .action(async (options?: UserProfileUpdateOptions) => { try { const result = await rettiwt.user.updateProfile({ name: options?.name, url: options?.url, location: options?.location, - description: options?.bio, + description: options?.description, }); output(result); } catch (error) { @@ -309,4 +309,14 @@ type UserAnalyticsOptions = { verifiedFollowers?: boolean; }; +/** + * The options for updating user profile. + */ +type UserProfileUpdateOptions = { + name?: string; + url?: string; + location?: string; + description?: string; +}; + export default createUserCommand; diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 04303612..80d4796b 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -1,6 +1,7 @@ import { Extractors } from '../../collections/Extractors'; import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; import { ResourceType } from '../../enums/Resource'; +import { ProfileUpdateOptions } from '../../models/args/ProfileArgs'; import { Analytics } from '../../models/data/Analytics'; import { CursoredData } from '../../models/data/CursoredData'; import { List } from '../../models/data/List'; @@ -8,6 +9,7 @@ import { Notification } from '../../models/data/Notification'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates'; import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics'; import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; @@ -24,8 +26,6 @@ import { IUserMediaResponse } from '../../types/raw/user/Media'; import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; import { IUserProfileUpdateResponse } from '../../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../../types/raw/user/Recommended'; -import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; -import { ProfileUpdateOptions } from '../../models/args/ProfileArgs'; import { IUserSubscriptionsResponse } from '../../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../../types/raw/user/TweetsAndReplies'; From a1038ab554d63d7a1a132108724ee468d7c5976d Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 1 Dec 2025 12:03:03 +0000 Subject: [PATCH 093/119] Fixed a compilation error that might occur under some compiler configurations --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/public/FetcherService.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8d0f50c..3f599b58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "https-proxy-agent": "^7.0.6", "jsdom": "^27.2.0", "node-html-parser": "^7.0.1", - "x-client-transaction-id": "^0.1.8" + "x-client-transaction-id": "^0.1.9" }, "bin": { "rettiwt": "dist/cli.js" @@ -5033,9 +5033,9 @@ } }, "node_modules/x-client-transaction-id": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.8.tgz", - "integrity": "sha512-0wYNIEKj124pKBHGWYb8Ux8CwtcUPAeUiqwM0KjW+NxGnuHQ3CnJF9k8rf1KekjbY8rVz37wXnKzrRPfaqnBMQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.9.tgz", + "integrity": "sha512-CES4zgkJ0wbfFWm0qgdKphthyb+L7lVHymgOY15v6ivcWSx5p9lp5kzAed+BuqJSP7bS0GbQyJ16ONkRthgsUw==", "license": "MIT", "dependencies": { "linkedom": "^0.18.9" diff --git a/package.json b/package.json index a28e0288..88774497 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "https-proxy-agent": "^7.0.6", "jsdom": "^27.2.0", "node-html-parser": "^7.0.1", - "x-client-transaction-id": "^0.1.8" + "x-client-transaction-id": "^0.1.9" }, "workspaces": [ "playground", diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 55fbde8f..2a11b9c9 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,7 +1,7 @@ import axios, { isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; import { JSDOM } from 'jsdom'; -import { ClientTransaction } from 'x-client-transaction-id-glacier'; +import { ClientTransaction } from 'x-client-transaction-id'; import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; import { Requests } from '../../collections/Requests'; From 87f666516aebe6dc98b6561b9bba507290b13e04 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 1 Dec 2025 12:04:45 +0000 Subject: [PATCH 094/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f599b58..e82d7111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.0", + "version": "6.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.0", + "version": "6.1.1", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 88774497..4fb6ba0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.0", + "version": "6.1.1", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From ce432d4f8402d2332f3349681a68bb6c702ed158 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 3 Dec 2025 21:22:17 +0530 Subject: [PATCH 095/119] Fixed an issue where user notification threw an `undefined` error, due to incorrect data type (#803) --- src/models/data/Notification.ts | 32 +- src/types/raw/base/Notification.ts | 71 ++-- src/types/raw/user/Notifications.ts | 500 +++++++++++++++++++++++----- 3 files changed, 461 insertions(+), 142 deletions(-) diff --git a/src/models/data/Notification.ts b/src/models/data/Notification.ts index 9a00ad51..fade68d3 100644 --- a/src/models/data/Notification.ts +++ b/src/models/data/Notification.ts @@ -1,9 +1,7 @@ import { NotificationType } from '../../enums/Notification'; -import { RawNotificationType } from '../../enums/raw/Notification'; -import { findKeyByValue } from '../../helper/JsonUtils'; +import { findByFilter } from '../../helper/JsonUtils'; import { INotification } from '../../types/data/Notification'; import { INotification as IRawNotification } from '../../types/raw/base/Notification'; -import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; /** * The details of a single notification. @@ -28,16 +26,16 @@ export class Notification implements INotification { this._raw = { ...notification }; // Getting the original notification type - const notificationType: string | undefined = findKeyByValue(RawNotificationType, notification.icon.id); + const notificationType = notification.notification_icon.toString(); - this.from = notification.template?.aggregateUserActionsV1?.fromUsers - ? notification.template.aggregateUserActionsV1.fromUsers.map((item) => item.user.id) + this.from = notification.template.from_users + ? notification.template.from_users.map((item) => item.user_results.result.rest_id) : []; this.id = notification.id; - this.message = notification.message.text; - this.receivedAt = new Date(Number(notification.timestampMs)).toISOString(); - this.target = notification.template?.aggregateUserActionsV1?.targetObjects - ? notification.template.aggregateUserActionsV1.targetObjects.map((item) => item.tweet.id) + this.message = notification.rich_message.text; + this.receivedAt = new Date(notification.timestamp_ms).toISOString(); + this.target = notification.template.target_objects + ? notification.template.target_objects.map((item) => item.tweet_results.result.rest_id) : []; this.type = notificationType ? NotificationType[notificationType as keyof typeof NotificationType] @@ -59,14 +57,12 @@ export class Notification implements INotification { public static list(response: NonNullable): Notification[] { const notifications: Notification[] = []; - // Extracting notifications - if ((response as IUserNotificationsResponse).globalObjects.notifications) { - // Iterating over the raw list of notifications - for (const [, value] of Object.entries( - (response as IUserNotificationsResponse).globalObjects.notifications, - )) { - notifications.push(new Notification(value as IRawNotification)); - } + // Extracting the matching data + const extract = findByFilter(response, '__typename', 'TimelineNotification'); + + // Deserializing valid data + for (const item of extract) { + notifications.push(new Notification(item)); } return notifications; diff --git a/src/types/raw/base/Notification.ts b/src/types/raw/base/Notification.ts index a3ceb04e..ebd81215 100644 --- a/src/types/raw/base/Notification.ts +++ b/src/types/raw/base/Notification.ts @@ -1,6 +1,9 @@ /* eslint-disable */ import { RawNotificationType } from '../../../enums/raw/Notification'; +import { IDataResult } from '../composite/DataResult'; +import { ITweet } from './Tweet'; +import { IUser } from './User'; /** * Represents the raw data of a single Notification. @@ -8,60 +11,56 @@ import { RawNotificationType } from '../../../enums/raw/Notification'; * @public */ export interface INotification { + itemType: string; + __typename: string; id: string; - timestampMs: string; - icon: INotificationIcon; - message: INotificationMessage; - template: INotificationTemplate; + notification_icon: RawNotificationType; + rich_message: INotificationRichMessage; + notification_url: INotificationUrl; + template: INoticiationTemplate; + timestamp_ms: string; } -export interface INotificationIcon { - id: RawNotificationType; -} - -export interface INotificationMessage { - text: string; - entities: INotificationMessageEntity[]; +export interface INotificationRichMessage { rtl: boolean; + text: string; + entities: INotificationEntity[]; } -export interface INotificationMessageEntity { +export interface INotificationEntity { fromIndex: number; toIndex: number; - format: string; + ref: INotificationEntityRef; } -export interface INotificationTemplate { - aggregateUserActionsV1: INotificationUserActions; +export interface INotificationEntityRef { + type: string; + user_results: IDataResult; } -export interface INotificationUserActions { - targetObjects: INotificationTargetObject[]; - fromUsers: INotificationFromUser[]; - additionalContext: INotificationAdditionalContext; +export interface INotificationUrl { + url: string; + urlType: string; + urtEndpointOptions?: INotificationUrtEndpointOptions; } -export interface INotificationTargetObject { - tweet: INotificationTweet; +export interface INotificationUrtEndpointOptions { + cacheId: string; + title: string; } -export interface INotificationTweet { - id: string; +export interface INoticiationTemplate { + __typename: string; + target_objects: INotificationTargetObject[]; + from_users: INotificationFromUser[]; } -export interface INotificationFromUser { - user: INotificationUser; -} - -export interface INotificationUser { - id: string; -} - -export interface INotificationAdditionalContext { - contextText: INotificationContextText; +export interface INotificationTargetObject { + __typename: string; + tweet_results: IDataResult; } -export interface INotificationContextText { - text: string; - entities: any[]; +export interface INotificationFromUser { + __typename: string; + user_results: IDataResult; } diff --git a/src/types/raw/user/Notifications.ts b/src/types/raw/user/Notifications.ts index a2440a48..ef82e698 100644 --- a/src/types/raw/user/Notifications.ts +++ b/src/types/raw/user/Notifications.ts @@ -6,141 +6,481 @@ * @public */ export interface IUserNotificationsResponse { - globalObjects: GlobalObjects; - timeline: Timeline; + data: Data; +} + +interface Data { + viewer_v2: ViewerV2; +} + +interface ViewerV2 { + user_results: UserResults; } -interface GlobalObjects { - notifications: Notifications; +interface UserResults { + result: Result; } -interface Notifications { - [key: string]: Notification; +interface Result { + __typename: string; + rest_id: string; + notification_timeline: NotificationTimeline; } -interface Notification { +interface NotificationTimeline { id: string; - timestampMs: string; - icon: Icon; - message: Message; - template: Template; + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; + sort_index?: string; } -interface Icon { +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + value?: string; + cursorType?: string; + itemContent?: ItemContent; + clientEventInfo?: ClientEventInfo; +} + +interface ItemContent { + itemType: string; + __typename: string; id: string; + notification_icon: string; + rich_message: RichMessage; + notification_url: NotificationUrl; + template: Template; + timestamp_ms: string; } -interface Message { +interface RichMessage { + rtl: boolean; text: string; entities: Entity[]; - rtl: boolean; } interface Entity { fromIndex: number; toIndex: number; - format: string; + ref: Ref; } -interface Template { - aggregateUserActionsV1: AggregateUserActionsV1; +interface Ref { + type: string; + user_results: UserResults2; +} + +interface UserResults2 { + result: Result2; +} + +interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +interface AffiliatesHighlightedLabel {} + +interface Avatar { + image_url: string; } -interface AggregateUserActionsV1 { - targetObjects: TargetObject[]; - fromUsers: FromUser[]; - additionalContext: AdditionalContext; +interface Core { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + notifications: boolean; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +interface Entities { + description: Description; +} + +interface Description { + urls: any[]; +} + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface ProfileBio { + description: string; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + followed_by: boolean; + following: boolean; +} + +interface TipjarSettings {} + +interface Verification { + verified: boolean; +} + +interface NotificationUrl { + url: string; + urlType: string; + urtEndpointOptions?: UrtEndpointOptions; +} + +interface UrtEndpointOptions { + cacheId: string; + title: string; +} + +interface Template { + __typename: string; + target_objects: TargetObject[]; + from_users: FromUser[]; } interface TargetObject { - tweet: Tweet; + __typename: string; + tweet_results: TweetResults; } -interface Tweet { - id: string; +interface TweetResults { + result: Result3; } -interface FromUser { - user: User; +interface Result3 { + __typename: string; + rest_id: string; + core: Core2; + unmention_data: UnmentionData; + edit_control: EditControl; + is_translatable: boolean; + views: Views; + source: string; + grok_analysis_button: boolean; + legacy: Legacy3; +} + +interface Core2 { + user_results: UserResults3; +} + +interface UserResults3 { + result: Result4; } -interface User { +interface Result4 { + __typename: string; id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel2; + avatar: Avatar2; + core: Core3; + dm_permissions: DmPermissions2; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy2; + location: Location2; + media_permissions: MediaPermissions2; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio2; + privacy: Privacy2; + relationship_perspectives: RelationshipPerspectives2; + tipjar_settings: TipjarSettings2; + verification: Verification2; + verified_phone_status: boolean; } -interface AdditionalContext { - contextText: ContextText; +interface AffiliatesHighlightedLabel2 {} + +interface Avatar2 { + image_url: string; } -interface ContextText { - text: string; - entities: any[]; +interface Core3 { + created_at: string; + name: string; + screen_name: string; } -interface Timeline { - id: string; - instructions: Instruction[]; +interface DmPermissions2 { + can_dm: boolean; } -interface Instruction { - clearCache?: ClearCache; - addEntries?: AddEntries; - clearEntriesUnreadState?: ClearEntriesUnreadState; - markEntriesUnreadGreaterThanSortIndex?: MarkEntriesUnreadGreaterThanSortIndex; +interface Legacy2 { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities2; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; } -interface ClearCache {} +interface Entities2 { + description: Description2; +} -interface AddEntries { - entries: Entry[]; +interface Description2 { + urls: any[]; } -interface Entry { - entryId: string; - sortIndex: string; - content: Content; +interface Location2 { + location: string; } -interface Content { - operation?: Operation; - item?: Item; +interface MediaPermissions2 { + can_media_tag: boolean; } -interface Operation { - cursor: Cursor; +interface ProfileBio2 { + description: string; } -interface Cursor { - value: string; - cursorType: string; +interface Privacy2 { + protected: boolean; } -interface Item { - content: Content2; - clientEventInfo: ClientEventInfo; - feedbackInfo?: FeedbackInfo; +interface RelationshipPerspectives2 { + following: boolean; } -interface Content2 { - notification: Notification2; +interface TipjarSettings2 {} + +interface Verification2 { + verified: boolean; } -interface Notification2 { +interface UnmentionData {} + +interface EditControl { + edit_tweet_ids: string[]; + editable_until_msecs: string; + is_edit_eligible: boolean; + edits_remaining: string; +} + +interface Views { + count: string; + state: string; +} + +interface Legacy3 { + bookmark_count: number; + bookmarked: boolean; + created_at: string; + conversation_id_str: string; + display_text_range: number[]; + entities: Entities3; + favorite_count: number; + favorited: boolean; + full_text: string; + is_quote_status: boolean; + lang: string; + quote_count: number; + reply_count: number; + retweet_count: number; + retweeted: boolean; + user_id_str: string; + id_str: string; +} + +interface Entities3 { + hashtags: any[]; + symbols: any[]; + timestamps: any[]; + urls: any[]; + user_mentions: any[]; +} + +interface FromUser { + __typename: string; + user_results: UserResults4; +} + +interface UserResults4 { + result: Result5; +} + +interface Result5 { + __typename: string; id: string; - url: Url; - fromUsers: string[]; - targetTweets: string[]; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel3; + avatar: Avatar3; + core: Core4; + dm_permissions: DmPermissions3; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy4; + location: Location3; + media_permissions: MediaPermissions3; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio3; + privacy: Privacy3; + relationship_perspectives: RelationshipPerspectives3; + tipjar_settings: TipjarSettings3; + verification: Verification3; + verified_phone_status: boolean; } -interface Url { - urlType: string; - url: string; - urtEndpointOptions?: UrtEndpointOptions; +interface AffiliatesHighlightedLabel3 {} + +interface Avatar3 { + image_url: string; } -interface UrtEndpointOptions { - title: string; - cacheId: string; +interface Core4 { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions3 { + can_dm: boolean; +} + +interface Legacy4 { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities4; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + notifications: boolean; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +interface Entities4 { + description: Description3; +} + +interface Description3 { + urls: any[]; +} + +interface Location3 { + location: string; +} + +interface MediaPermissions3 { + can_media_tag: boolean; +} + +interface ProfileBio3 { + description: string; +} + +interface Privacy3 { + protected: boolean; +} + +interface RelationshipPerspectives3 { + followed_by: boolean; + following: boolean; +} + +interface TipjarSettings3 {} + +interface Verification3 { + verified: boolean; } interface ClientEventInfo { @@ -157,19 +497,3 @@ interface NotificationDetails { impressionId: string; metadata: string; } - -interface FeedbackInfo { - feedbackKeys: string[]; - feedbackMetadata: string; - clientEventInfo: ClientEventInfo2; -} - -interface ClientEventInfo2 { - element: string; -} - -interface ClearEntriesUnreadState {} - -interface MarkEntriesUnreadGreaterThanSortIndex { - sortIndex: string; -} From 26d4e299e8f0b8c5b56962f2c34a0d74291c1326 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 3 Dec 2025 15:56:01 +0000 Subject: [PATCH 096/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e82d7111..996b6434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.1", + "version": "6.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.1", + "version": "6.1.2", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 4fb6ba0f..af31b75b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.1", + "version": "6.1.2", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From b1b5fcf8c3399d047f7d8f94265a8591866784f3 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 12 Dec 2025 15:43:10 +0000 Subject: [PATCH 097/119] Fixed an issue where an error was still thrown even if custom error handler handled it --- src/services/public/FetcherService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 3e2189db..7c013ea6 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -354,7 +354,6 @@ export class FetcherService { // Else, delegate error handling else { this._errorHandler.handle(err); - throw err; } } finally { // Incrementing the number of retries done From a765a66bc3d4eae09f098a60ea9b6ace47a243c2 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 12 Dec 2025 15:46:48 +0000 Subject: [PATCH 098/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 996b6434..3d51148a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index af31b75b..ca7e0918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From d7260c95c3a4e8365ed69ace4e6b6832421d4564 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 12 Dec 2025 15:43:10 +0000 Subject: [PATCH 099/119] Fixed an issue where an error was still thrown even if custom error handler handled it --- src/services/public/FetcherService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 2a11b9c9..19b91767 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -333,7 +333,6 @@ export class FetcherService { // Else, delegate error handling else { this._errorHandler.handle(err); - throw err; } } finally { // Incrementing the number of retries done From 53c0490d9f83f4a32ff9824e9823eca0a01abce5 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Fri, 12 Dec 2025 15:46:48 +0000 Subject: [PATCH 100/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 996b6434..3d51148a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index af31b75b..ca7e0918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.2", + "version": "6.1.3", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 0cccac4a8a33123af36f78e7bf867c2cd2d5adc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20K=C3=B6yk=C4=B1ran?= Date: Sun, 23 Nov 2025 01:01:59 +0000 Subject: [PATCH 101/119] Fixed tweet.post() returning undefined on Error 226 (#796) --- src/services/public/FetcherService.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 19b91767..7c013ea6 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,4 +1,4 @@ -import axios, { isAxiosError } from 'axios'; +import axios, { AxiosError, isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; import { JSDOM } from 'jsdom'; import { ClientTransaction } from 'x-client-transaction-id'; @@ -11,11 +11,13 @@ import { ResourceType } from '../../enums/Resource'; import { FetchArgs } from '../../models/args/FetchArgs'; import { PostArgs } from '../../models/args/PostArgs'; import { AuthCredential } from '../../models/auth/AuthCredential'; +import { TwitterError } from '../../models/errors/TwitterError'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IFetchArgs } from '../../types/args/FetchArgs'; import { IPostArgs } from '../../types/args/PostArgs'; import { ITransactionHeader } from '../../types/auth/TransactionHeader'; import { IErrorHandler } from '../../types/ErrorHandler'; +import { IErrorData } from '../../types/raw/base/Error'; import { AuthService } from '../internal/AuthService'; import { ErrorService } from '../internal/ErrorService'; @@ -322,8 +324,27 @@ export class FetcherService { // Introducing a delay await this._wait(); + // Getting the response body + const responseData = (await axios(config)).data; + + // Check for Twitter API errors in response body + // Type guard to check if response contains errors + const potentialErrorResponse = responseData as unknown as Partial; + if (potentialErrorResponse.errors && Array.isArray(potentialErrorResponse.errors)) { + // Throw TwitterError using existing error class + const axiosError = { + response: { + data: { errors: potentialErrorResponse.errors }, + status: 200, + }, + message: potentialErrorResponse.errors[0]?.message ?? 'Twitter API Error', + status: 200, + } as AxiosError; + throw new TwitterError(axiosError); + } + // Returning the reponse body - return (await axios(config)).data; + return responseData; } catch (err) { // If it's an error 404, retry if (isAxiosError(err) && err.status === 404) { From 43775d2cc5e19fa249e1ed4e0aa6b408c413329e Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 16 Dec 2025 13:00:08 +0000 Subject: [PATCH 102/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d51148a..0fba94cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.3", + "version": "6.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.3", + "version": "6.1.4", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index ca7e0918..056a1f43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.3", + "version": "6.1.4", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 5fbfb58ec0743177fd5a8cd74a77bed527046afa Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 23 Dec 2025 05:36:09 +0000 Subject: [PATCH 103/119] Updated all Twitter API endpoints --- src/models/data/User.ts | 12 +-- src/requests/List.ts | 45 ++++++----- src/requests/Tweet.ts | 83 +++++++++++--------- src/requests/User.ts | 117 +++++++++++++++++++++-------- src/services/public/UserService.ts | 2 - src/types/raw/base/User.ts | 20 ++++- 6 files changed, 179 insertions(+), 100 deletions(-) diff --git a/src/models/data/User.ts b/src/models/data/User.ts index 4c7591e7..09f5a66e 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -37,9 +37,9 @@ export class User implements IUser { public constructor(user: IRawUser) { this._raw = { ...user }; this.id = user.rest_id; - this.userName = user.legacy.screen_name; - this.fullName = user.legacy.name; - this.createdAt = new Date(user.legacy.created_at).toISOString(); + this.userName = user.core?.screen_name ?? user.legacy.screen_name ?? ''; + this.fullName = user.core?.name ?? user.legacy.name ?? ''; + this.createdAt = new Date(user.core?.created_at ?? user.legacy.created_at ?? 0).toISOString(); this.description = user.legacy.description.length ? user.legacy.description : undefined; this.isFollowed = user.legacy.following; this.isFollowing = user.legacy.followed_by; @@ -48,7 +48,7 @@ export class User implements IUser { this.followersCount = user.legacy.followers_count; this.followingsCount = user.legacy.friends_count; this.statusesCount = user.legacy.statuses_count; - this.location = user.legacy.location.length ? user.legacy.location : undefined; + this.location = user.location?.location ?? user.legacy.location ?? undefined; this.pinnedTweet = user.legacy.pinned_tweet_ids_str[0]; this.profileBanner = user.legacy.profile_banner_url; this.profileImage = user.legacy.profile_image_url_https; @@ -75,7 +75,7 @@ export class User implements IUser { // Deserializing valid data for (const item of extract) { - if (item.legacy && item.legacy.created_at) { + if (item.legacy && (item.core?.created_at || item.legacy.created_at)) { // Logging LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); @@ -112,7 +112,7 @@ export class User implements IUser { // Deserializing valid data for (const item of extract) { - if (item.legacy && item.legacy.created_at) { + if (item.legacy && (item.core?.created_at || item.legacy.created_at)) { // Logging LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); diff --git a/src/requests/List.ts b/src/requests/List.ts index 6493bce4..9c8527f1 100644 --- a/src/requests/List.ts +++ b/src/requests/List.ts @@ -13,7 +13,7 @@ export class ListRequests { public static addMember(listId: string, userId: string): AxiosRequestConfig { return { method: 'post', - url: 'https://x.com/i/api/graphql/uFQumgzNDR27zs0yK5J3Fw/ListAddMember', + url: 'https://x.com/i/api/graphql/EadD8ivrhZhYQr2pDmCpjA/ListAddMember', data: { /* eslint-disable @typescript-eslint/naming-convention */ @@ -22,12 +22,12 @@ export class ListRequests { userId: userId, }, features: { - payments_enabled: false, - profile_label_improvements_pcf_label_in_post_enabled: false, - rweb_tipjar_consumption_enabled: false, - verified_phone_label_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, - responsive_web_graphql_timeline_navigation_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, }, /* eslint-enable @typescript-eslint/naming-convention */ @@ -41,13 +41,14 @@ export class ListRequests { public static details(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/gO1_eYPohKYHwCG2m-1ZnQ/ListByRestId', + url: 'https://x.com/i/api/graphql/Tzkkg-NaBi_y1aAUUb6_eQ/ListByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ listId: id }), features: JSON.stringify({ - rweb_lists_timeline_redesign_enabled: true, - responsive_web_graphql_exclude_directive_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true, @@ -66,7 +67,7 @@ export class ListRequests { public static members(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/T7VZsrWpCoi4jWxFdwyNcg/ListMembers', + url: 'https://x.com/i/api/graphql/Bnhcen0kdsMAU1tW7U79qQ/ListMembers', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -77,6 +78,7 @@ export class ListRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -87,7 +89,7 @@ export class ListRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -105,6 +107,8 @@ export class ListRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -119,7 +123,7 @@ export class ListRequests { public static removeMember(listId: string, userId: string): AxiosRequestConfig { return { method: 'post', - url: 'https://x.com/i/api/graphql/IzgPnK3wZpNgpcN31ry3Xg/ListRemoveMember', + url: 'https://x.com/i/api/graphql/B5tMzrMYuFHJex_4EXFTSw/ListRemoveMember', data: { /* eslint-disable @typescript-eslint/naming-convention */ @@ -128,12 +132,12 @@ export class ListRequests { userId: userId, }, features: { - payments_enabled: false, - profile_label_improvements_pcf_label_in_post_enabled: false, - rweb_tipjar_consumption_enabled: false, - verified_phone_label_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, - responsive_web_graphql_timeline_navigation_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, }, /* eslint-enable @typescript-eslint/naming-convention */ @@ -149,7 +153,7 @@ export class ListRequests { public static tweets(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/BkauSnPUDQTeeJsxq17opA/ListLatestTweetsTimeline', + url: 'https://x.com/i/api/graphql/fqNUs_6rqLf89u_2waWuqg/ListLatestTweetsTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -160,6 +164,7 @@ export class ListRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -170,7 +175,7 @@ export class ListRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -188,6 +193,8 @@ export class ListRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/requests/Tweet.ts b/src/requests/Tweet.ts index c9d1ae92..f72f6778 100644 --- a/src/requests/Tweet.ts +++ b/src/requests/Tweet.ts @@ -34,7 +34,7 @@ export class TweetRequests { public static bulkDetails(ids: string[]): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/kPnxYjNX2HCKu8aY96er5w/TweetResultsByRestIds', + url: 'https://x.com/i/api/graphql/-R17e8UqwApFGdMxa3jASA/TweetResultsByRestIds', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -83,6 +83,9 @@ export class TweetRequests { responsive_web_grok_analyze_button_fetch_trends_enabled: false, articles_preview_enabled: false, responsive_web_grok_share_attachment_enabled: false, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -96,7 +99,7 @@ export class TweetRequests { public static details(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/zAz9764BcLZOJ0JU2wrd1A/TweetResultByRestId', + url: 'https://x.com/i/api/graphql/aFvUsJm2c-oDkJV75blV6g/TweetResultByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -173,18 +176,20 @@ export class TweetRequests { public static likers(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/4AzoFlLkEcs2bx5pO1mvsQ/Favoriters', + url: 'https://x.com/i/api/graphql/b3OrdeHDQfb9zRMC0fV3bw/Favoriters', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ tweetId: id, count: count, cursor: cursor, + enableRanking: false, includePromotedContent: false, }), features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -195,7 +200,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -213,6 +218,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -230,7 +237,7 @@ export class TweetRequests { return { method: 'post', - url: 'https://x.com/i/api/graphql/IID9x6WsdMnTlXnzXGq8ng/CreateTweet', + url: 'https://x.com/i/api/graphql/Uf3io9zVp1DsYxrmL5FJ7g/CreateTweet', data: { /* eslint-disable @typescript-eslint/naming-convention */ variables: { @@ -271,6 +278,9 @@ export class TweetRequests { responsive_web_grok_image_annotation_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_enhance_cards_enabled: false, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_profile_redirect_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, }, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -284,7 +294,7 @@ export class TweetRequests { public static replies(id: string, cursor?: string, sortBy?: RawTweetRepliesSortType): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/_8aYOgEDz35BrBcBal1-_w/TweetDetail', + url: 'https://x.com/i/api/graphql/97JF30KziU00483E_8elBA/TweetDetail', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -302,6 +312,7 @@ export class TweetRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -312,7 +323,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -330,6 +341,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: JSON.stringify({ @@ -350,7 +363,7 @@ export class TweetRequests { public static retweet(id: string): AxiosRequestConfig { return { method: 'post', - url: 'https://x.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet', + url: 'https://x.com/i/api/graphql/LFho5rIi4xcKO90p9jwG7A/CreateRetweet', data: { variables: { /* eslint-disable @typescript-eslint/naming-convention */ @@ -370,7 +383,7 @@ export class TweetRequests { public static retweeters(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/i-CI8t2pJD15euZJErEDrg/Retweeters', + url: 'https://x.com/i/api/graphql/wfglZEC0MRgBdxMa_1a5YQ/Retweeters', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -382,6 +395,7 @@ export class TweetRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -392,7 +406,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -410,6 +424,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -458,7 +474,7 @@ export class TweetRequests { return { method: 'get', - url: 'https://x.com/i/api/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline', + url: 'https://x.com/i/api/graphql/M1jEez78PEfVfbQLvlWMvQ/SearchTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -467,48 +483,43 @@ export class TweetRequests { cursor: cursor, querySource: 'typed_query', product: parsedFilter.top ? RawTweetSearchResultType.TOP : RawTweetSearchResultType.LATEST, - withAuxiliaryUserLabels: false, - withArticleRichContentState: false, - withArticlePlainText: false, - withGrokAnalyze: false, - withDisallowedReplyControls: false, + withGrokTranslatedBio: false, }), features: JSON.stringify({ - rweb_lists_timeline_redesign_enabled: true, - responsive_web_graphql_exclude_directive_enabled: true, + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, - tweetypie_unmention_optimization_enabled: true, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, - responsive_web_twitter_article_tweet_consumption_enabled: false, + responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, - responsive_web_media_download_video_enabled: false, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, - c9s_tweet_anatomy_moderator_badge_enabled: false, - responsive_web_grok_show_grok_translated_post: false, - premium_content_api_read_enabled: false, - rweb_video_screen_enabled: false, - responsive_web_grok_analyze_post_followups_enabled: false, - creator_subscriptions_quote_tweet_preview_enabled: false, - communities_web_enable_tweet_community_results_fetch: false, - rweb_tipjar_consumption_enabled: false, - responsive_web_grok_analyze_button_fetch_trends_enabled: false, - profile_label_improvements_pcf_label_in_post_enabled: false, - responsive_web_grok_image_annotation_enabled: false, - responsive_web_jetfuel_frame: false, - articles_preview_enabled: false, - responsive_web_grok_share_attachment_enabled: false, - responsive_web_grok_analysis_button_from_backend: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, diff --git a/src/requests/User.ts b/src/requests/User.ts index 438ed40c..9061c7e3 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -18,7 +18,7 @@ export class UserRequests { public static affiliates(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/OVFfg1hExk_AygiMVSJd-Q/UserBusinessProfileTeamTimeline', + url: 'https://x.com/i/api/graphql/KFaAofDlKP7bnzskNWmjwA/UserBusinessProfileTeamTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -33,6 +33,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -43,7 +44,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -61,6 +62,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -110,7 +113,7 @@ export class UserRequests { public static bookmarks(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/-LGfdImKeQz0xS_jjUwzlA/Bookmarks', + url: 'https://x.com/i/api/graphql/E6jlrZG4703s0mcA9DfNKQ/Bookmarks', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -121,6 +124,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -131,7 +135,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -149,6 +153,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -163,7 +169,7 @@ export class UserRequests { public static bulkDetailsByIds(ids: string[]): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/PyRggX3LQweP9nSF6PHliA/UsersByRestIds', + url: 'https://x.com/i/api/graphql/xavgLWWbFH8wm_8MQN8plQ/UsersByRestIds', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ userIds: ids }), @@ -179,6 +185,7 @@ export class UserRequests { responsive_web_graphql_timeline_navigation_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: false, rweb_tipjar_consumption_enabled: false, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -192,7 +199,7 @@ export class UserRequests { public static detailsById(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/WJ7rCtezBVT6nk6VM5R8Bw/UserByRestId', + url: 'https://x.com/i/api/graphql/Bbaot8ySMtJD7K2t01gW7A/UserByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ userId: id, withSafetyModeUserFields: true }), @@ -207,6 +214,7 @@ export class UserRequests { creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -220,13 +228,14 @@ export class UserRequests { public static detailsByUsername(userName: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/1VOOyvKkiI3FMmkeDNxM9A/UserByScreenName', + url: 'https://x.com/i/api/graphql/-oaLodhGbbnzJBACb1kk2Q/UserByScreenName', params: { /* eslint-disable @typescript-eslint/naming-convention */ - variables: JSON.stringify({ screen_name: userName, withSafetyModeUserFields: true }), + variables: JSON.stringify({ screen_name: userName, withGrokTranslatedBio: false }), features: JSON.stringify({ hidden_profile_subscriptions_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, subscriptions_verification_info_is_identity_verified_enabled: true, @@ -267,7 +276,7 @@ export class UserRequests { public static followed(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/CRprHpVA12yhsub-KRERIg/HomeLatestTimeline', + url: 'https://x.com/i/api/graphql/_qO7FJzShSKYWi9gtboE6A/HomeLatestTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -280,6 +289,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -290,7 +300,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -308,6 +318,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -324,7 +336,7 @@ export class UserRequests { public static followers(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/Elc_-qTARceHpztqhI9PQA/Followers', + url: 'https://x.com/i/api/graphql/kuFUYP9eV1FPoEy4N-pi7w/Followers', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -332,10 +344,12 @@ export class UserRequests { count: count, cursor: cursor, includePromotedContent: false, + withGrokTranslatedBio: false, }), features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -346,7 +360,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -364,6 +378,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -436,7 +452,7 @@ export class UserRequests { public static highlights(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/cr8FsaThDCa9LKeD9CNZ4w/UserHighlightsTweets', + url: 'https://x.com/i/api/graphql/kzKWdUA6Y1LCqlvaVILZwQ/UserHighlightsTweets', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -449,6 +465,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -459,7 +476,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -477,6 +494,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -494,7 +513,7 @@ export class UserRequests { public static likes(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/eQl7iWsCr2fChppuJdAeRw/Likes', + url: 'https://x.com/i/api/graphql/JR2gceKucIKcVNB_9JkhsA/Likes', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -510,6 +529,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -520,7 +540,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -538,6 +558,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -555,15 +577,14 @@ export class UserRequests { public static lists(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/tZg2CHWw-NAL0nKO2Q-P4Q/ListsManagementPageTimeline', + url: 'https://x.com/i/api/graphql/9mQl9vR31wjodBP9b7_wyQ/ListsManagementPageTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ count: 100, cursor: cursor }), features: JSON.stringify({ rweb_video_screen_enabled: false, - payments_enabled: false, - rweb_xchat_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -592,6 +613,7 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), @@ -610,7 +632,7 @@ export class UserRequests { public static media(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/vFPc2LVIu7so2uA_gHQAdg/UserMedia', + url: 'https://x.com/i/api/graphql/MMnr49cP_nldzCTfeVDRtA/UserMedia', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -626,6 +648,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -636,7 +659,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -654,6 +677,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -670,7 +695,7 @@ export class UserRequests { public static notifications(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/gaBVLalXDBRDJz6maKgdWg/NotificationsTimeline', + url: 'https://x.com/i/api/graphql/Ev6UMJRROInk_RMH2oVbBg/NotificationsTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -681,6 +706,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -691,7 +717,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -709,6 +735,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -723,7 +751,7 @@ export class UserRequests { public static recommended(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/Q_P3YVnmHunGFkZ8ISM-7w/HomeTimeline', + url: 'https://x.com/i/api/graphql/V7xdnRnvW6a8vIsMr9xK7A/HomeTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -737,6 +765,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -747,7 +776,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -765,6 +794,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -792,7 +823,7 @@ export class UserRequests { public static subscriptions(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/UWlxAhUnBNK0BYmeqNPqAw/UserCreatorSubscriptions', + url: 'https://x.com/i/api/graphql/fl06vhYypYRcRxgLKO011Q/UserCreatorSubscriptions', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -802,25 +833,39 @@ export class UserRequests { includePromotedContent: false, }), features: JSON.stringify({ - responsive_web_graphql_exclude_directive_enabled: true, + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, - tweetypie_unmention_optimization_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, - rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -837,7 +882,7 @@ export class UserRequests { public static tweets(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/HeWHY26ItCfUmm1e6ITjeA/UserTweets', + url: 'https://x.com/i/api/graphql/-V26I6Pb5xDZ3C7BWwCQ_Q/UserTweets', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -852,6 +897,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -862,7 +908,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -880,6 +926,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -897,7 +945,7 @@ export class UserRequests { public static tweetsAndReplies(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/OAx9yEcW3JA9bPo63pcYlA/UserTweetsAndReplies', + url: 'https://x.com/i/api/graphql/61HQnvcGP870hiE-hCbG4A/UserTweetsAndReplies', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -912,6 +960,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -922,7 +971,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -940,6 +989,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 46b69528..ee65288b 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -830,8 +830,6 @@ export class UserService extends FetcherService { /** * Get the list of subscriptions of a user. * - * @deprecated Currently not working. - * * @param id - The ID of the target user. If no ID is provided, the logged-in user's ID is used. * @param count - The number of subscriptions to fetch, must be \<= 100. * @param cursor - The cursor to the batch of subscriptions to fetch. diff --git a/src/types/raw/base/User.ts b/src/types/raw/base/User.ts index 81060557..bce99124 100644 --- a/src/types/raw/base/User.ts +++ b/src/types/raw/base/User.ts @@ -11,11 +11,13 @@ export interface IUser { __typename: string; id: string; rest_id: string; + core?: IUserCore; affiliates_highlighted_label: IAffiliatesHighlightedLabel; has_graduated_access: boolean; is_blue_verified: boolean; profile_image_shape: string; legacy: IUserLegacy; + location?: IUserLocation; super_follow_eligible: boolean; smart_blocked_by: boolean; smart_blocking: boolean; @@ -28,6 +30,12 @@ export interface IUser { creator_subscriptions_count: number; } +export interface IUserCore { + created_at: string; + name: string; + screen_name: string; +} + export interface IAffiliatesHighlightedLabel { label: IAffiliateLabel; } @@ -82,11 +90,14 @@ export interface IAffiliateHighlightedMentionResultLegacy { } export interface IUserLegacy { + created_at?: string; + name?: string; + screen_name?: string; + location?: string; followed_by?: boolean; following?: boolean; can_dm: boolean; can_media_tag: boolean; - created_at: string; default_profile: boolean; default_profile_image: boolean; description: string; @@ -98,16 +109,13 @@ export interface IUserLegacy { has_custom_timelines: boolean; is_translator: boolean; listed_count: number; - location: string; media_count: number; - name: string; normal_followers_count: number; pinned_tweet_ids_str: string[]; possibly_sensitive: boolean; profile_banner_url: string; profile_image_url_https: string; profile_interstitial_type: string; - screen_name: string; statuses_count: number; translator_type: string; verified: boolean; @@ -128,6 +136,10 @@ export interface IProfileUrl { urls: IUrl[]; } +export interface IUserLocation { + location: string; +} + export interface ILegacyExtendedProfile {} export interface IVerificationInfo { From bbb1eb489059e6289ce16f2672a3e839f507b67e Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 23 Dec 2025 05:38:39 +0000 Subject: [PATCH 104/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fba94cb..9bc15c06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.4", + "version": "6.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.4", + "version": "6.1.5", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 056a1f43..b6da4293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.4", + "version": "6.1.5", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From e791ca4303b2a36b289033c9d3dc4898beebf4c8 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 23 Dec 2025 07:19:42 +0000 Subject: [PATCH 105/119] Fixed an issue where error would be thrown even when request was partially successful --- src/services/public/FetcherService.ts | 7 ++++++- src/types/raw/base/Error.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 7c013ea6..8c1947ba 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -330,7 +330,12 @@ export class FetcherService { // Check for Twitter API errors in response body // Type guard to check if response contains errors const potentialErrorResponse = responseData as unknown as Partial; - if (potentialErrorResponse.errors && Array.isArray(potentialErrorResponse.errors)) { + if ( + potentialErrorResponse.errors && + Array.isArray(potentialErrorResponse.errors) && + (potentialErrorResponse.data === undefined || + JSON.stringify(potentialErrorResponse.data) === JSON.stringify({})) + ) { // Throw TwitterError using existing error class const axiosError = { response: { diff --git a/src/types/raw/base/Error.ts b/src/types/raw/base/Error.ts index 918d216b..c9ff814d 100644 --- a/src/types/raw/base/Error.ts +++ b/src/types/raw/base/Error.ts @@ -6,6 +6,7 @@ * @public */ export interface IErrorData { + data: unknown; errors: IErrorDetails[]; } From 66e2961f420a00510585057d2f7aff18b3239d0a Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 23 Dec 2025 07:21:08 +0000 Subject: [PATCH 106/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bc15c06..3bef8c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.5", + "version": "6.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.5", + "version": "6.1.6", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index b6da4293..0aac6f2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.5", + "version": "6.1.6", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 3938c066f24c9885600480d3790289cbc54355f6 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 24 Dec 2025 10:53:56 +0000 Subject: [PATCH 107/119] Fixed an issue where profile image was missing from deserialized user --- src/models/data/User.ts | 2 +- src/types/raw/base/User.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/models/data/User.ts b/src/models/data/User.ts index 09f5a66e..7259baae 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -51,7 +51,7 @@ export class User implements IUser { this.location = user.location?.location ?? user.legacy.location ?? undefined; this.pinnedTweet = user.legacy.pinned_tweet_ids_str[0]; this.profileBanner = user.legacy.profile_banner_url; - this.profileImage = user.legacy.profile_image_url_https; + this.profileImage = user.avatar?.image_url ?? user.legacy.profile_image_url_https ?? ''; } /** The raw user details. */ diff --git a/src/types/raw/base/User.ts b/src/types/raw/base/User.ts index bce99124..218c9359 100644 --- a/src/types/raw/base/User.ts +++ b/src/types/raw/base/User.ts @@ -12,6 +12,7 @@ export interface IUser { id: string; rest_id: string; core?: IUserCore; + avatar?: IUserAvatar; affiliates_highlighted_label: IAffiliatesHighlightedLabel; has_graduated_access: boolean; is_blue_verified: boolean; @@ -36,6 +37,10 @@ export interface IUserCore { screen_name: string; } +export interface IUserAvatar { + image_url: string; +} + export interface IAffiliatesHighlightedLabel { label: IAffiliateLabel; } @@ -114,7 +119,7 @@ export interface IUserLegacy { pinned_tweet_ids_str: string[]; possibly_sensitive: boolean; profile_banner_url: string; - profile_image_url_https: string; + profile_image_url_https?: string; profile_interstitial_type: string; statuses_count: number; translator_type: string; From adacaeaa6943fc2180d36f39e20737dc8ca10e57 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 24 Dec 2025 10:57:05 +0000 Subject: [PATCH 108/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bef8c4e..913455d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.6", + "version": "6.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.6", + "version": "6.1.7", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 0aac6f2a..31850760 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.6", + "version": "6.1.7", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 1ce64e16f02a7fd9deb9c3c47a0505844130ee7a Mon Sep 17 00:00:00 2001 From: Steven <76638078+WXL-steven@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:06:50 +0800 Subject: [PATCH 109/119] feat(tweet): add media id to TweetMedia --- src/models/data/Tweet.ts | 10 ++++++++-- src/types/data/Tweet.ts | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index c75a5660..74320800 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -272,7 +272,7 @@ export class Tweet implements ITweet { * * @public */ -export class TweetEntities { +export class TweetEntities implements ITweetEntities { /** The list of hashtags mentioned in the tweet. */ public hashtags: string[] = []; @@ -325,7 +325,7 @@ export class TweetEntities { * * @public */ -export class TweetMedia { +export class TweetMedia implements ITweetMedia { /** The thumbnail URL for the video content of the tweet. */ public thumbnailUrl?: string; @@ -335,10 +335,15 @@ export class TweetMedia { /** The direct URL to the media. */ public url = ''; + /** The ID of the media. */ + public id: string; + /** * @param media - The raw media details. */ public constructor(media: IRawExtendedMedia) { + this.id = media.id_str; + // If the media is a photo if (media.type == RawMediaType.PHOTO) { this.type = MediaType.PHOTO; @@ -374,6 +379,7 @@ export class TweetMedia { */ public toJSON(): ITweetMedia { return { + id: this.id, thumbnailUrl: this.thumbnailUrl, type: this.type, url: this.url, diff --git a/src/types/data/Tweet.ts b/src/types/data/Tweet.ts index 1cb1add6..4fd4d7da 100644 --- a/src/types/data/Tweet.ts +++ b/src/types/data/Tweet.ts @@ -85,6 +85,9 @@ export interface ITweetEntities { * @public */ export interface ITweetMedia { + /** The ID of the media. */ + id: string; + /** The thumbnail URL for the video content of the tweet. */ thumbnailUrl?: string; From 62c5758e6dbc2d9cb9c01aed90efdb6bd79fbd03 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Tue, 30 Dec 2025 14:07:04 +0000 Subject: [PATCH 110/119] Linted and formatted code --- src/models/data/Tweet.ts | 10 +++++----- src/types/data/Tweet.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index 74320800..92c86dd6 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -326,6 +326,9 @@ export class TweetEntities implements ITweetEntities { * @public */ export class TweetMedia implements ITweetMedia { + /** The ID of the media. */ + public id: string; + /** The thumbnail URL for the video content of the tweet. */ public thumbnailUrl?: string; @@ -335,14 +338,11 @@ export class TweetMedia implements ITweetMedia { /** The direct URL to the media. */ public url = ''; - /** The ID of the media. */ - public id: string; - /** * @param media - The raw media details. */ public constructor(media: IRawExtendedMedia) { - this.id = media.id_str; + this.id = media.id_str; // If the media is a photo if (media.type == RawMediaType.PHOTO) { @@ -379,7 +379,7 @@ export class TweetMedia implements ITweetMedia { */ public toJSON(): ITweetMedia { return { - id: this.id, + id: this.id, thumbnailUrl: this.thumbnailUrl, type: this.type, url: this.url, diff --git a/src/types/data/Tweet.ts b/src/types/data/Tweet.ts index 4fd4d7da..199b8276 100644 --- a/src/types/data/Tweet.ts +++ b/src/types/data/Tweet.ts @@ -87,7 +87,7 @@ export interface ITweetEntities { export interface ITweetMedia { /** The ID of the media. */ id: string; - + /** The thumbnail URL for the video content of the tweet. */ thumbnailUrl?: string; From 2bdbc19652aee89e7302835690de2a81eabee2c6 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Wed, 7 Jan 2026 14:07:55 +0000 Subject: [PATCH 111/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 913455d9..16d175ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.1.7", + "version": "6.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.1.7", + "version": "6.2.0", "license": "ISC", "workspaces": [ "playground", diff --git a/package.json b/package.json index 31850760..56909735 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.1.7", + "version": "6.2.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", From 0a71095e5878b7a09c2d3414ab315bfdfec53180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cavalheiro?= Date: Tue, 23 Dec 2025 17:55:39 +0000 Subject: [PATCH 112/119] Enabled query to bookmark folders and their respective tweets --- src/collections/Extractors.ts | 7 ++ src/collections/Groups.ts | 2 + src/collections/Requests.ts | 2 + src/enums/Data.ts | 1 + src/enums/Resource.ts | 2 + src/models/data/BookmarkFolder.ts | 71 ++++++++++++ src/models/data/CursoredData.ts | 8 +- src/requests/User.ts | 120 +++++++++++++++++++++ src/services/public/UserService.ts | 85 +++++++++++++++ src/types/data/BookmarkFolder.ts | 12 +++ src/types/data/CursoredData.ts | 3 +- src/types/raw/base/BookmarkFolder.ts | 14 +++ src/types/raw/user/BookmarkFolderTweets.ts | 53 +++++++++ src/types/raw/user/BookmarkFolders.ts | 41 +++++++ 14 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 src/models/data/BookmarkFolder.ts create mode 100644 src/types/data/BookmarkFolder.ts create mode 100644 src/types/raw/base/BookmarkFolder.ts create mode 100644 src/types/raw/user/BookmarkFolderTweets.ts create mode 100644 src/types/raw/user/BookmarkFolders.ts diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index ef5b0239..5d9ba7a2 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,5 +1,6 @@ import { BaseType } from '../enums/Data'; import { Analytics } from '../models/data/Analytics'; +import { BookmarkFolder } from '../models/data/BookmarkFolder'; import { Conversation } from '../models/data/Conversation'; import { CursoredData } from '../models/data/CursoredData'; import { Inbox } from '../models/data/Inbox'; @@ -35,6 +36,8 @@ import { ITweetUnscheduleResponse } from '../types/raw/tweet/Unschedule'; import { IUserAffiliatesResponse } from '../types/raw/user/Affiliates'; import { IUserAnalyticsResponse } from '../types/raw/user/Analytics'; import { IUserBookmarksResponse } from '../types/raw/user/Bookmarks'; +import { IUserBookmarkFoldersResponse } from '../types/raw/user/BookmarkFolders'; +import { IUserBookmarkFolderTweetsResponse } from '../types/raw/user/BookmarkFolderTweets'; import { IUserDetailsResponse } from '../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../types/raw/user/DetailsBulk'; import { IUserFollowResponse } from '../types/raw/user/Follow'; @@ -111,6 +114,10 @@ export const Extractors = { new Analytics(response.data.viewer_v2.user_results.result), USER_BOOKMARKS: (response: IUserBookmarksResponse): CursoredData => new CursoredData(response, BaseType.TWEET), + USER_BOOKMARK_FOLDERS: (response: IUserBookmarkFoldersResponse): CursoredData => + new CursoredData(response, BaseType.BOOKMARK_FOLDER), + USER_BOOKMARK_FOLDER_TWEETS: (response: IUserBookmarkFolderTweetsResponse): CursoredData => + new CursoredData(response, BaseType.TWEET), USER_DETAILS_BY_USERNAME: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_ID: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_IDS_BULK: (response: IUserDetailsBulkResponse, ids: string[]): User[] => diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 3b53c688..050adc76 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -33,6 +33,8 @@ export const FetchResourcesGroup = [ ResourceType.USER_AFFILIATES, ResourceType.USER_ANALYTICS, ResourceType.USER_BOOKMARKS, + ResourceType.USER_BOOKMARK_FOLDERS, + ResourceType.USER_BOOKMARK_FOLDER_TWEETS, ResourceType.USER_DETAILS_BY_USERNAME, ResourceType.USER_DETAILS_BY_ID, ResourceType.USER_DETAILS_BY_IDS_BULK, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 48d4e09a..3cc1ce4d 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -63,6 +63,8 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | args.showVerifiedFollowers!, ), USER_BOOKMARKS: (args: IFetchArgs) => UserRequests.bookmarks(args.count, args.cursor), + USER_BOOKMARK_FOLDERS: (args: IFetchArgs) => UserRequests.bookmarkFolders(args.cursor), + USER_BOOKMARK_FOLDER_TWEETS: (args: IFetchArgs) => UserRequests.bookmarkFolderTweets(args.id!, args.count, args.cursor), USER_DETAILS_BY_USERNAME: (args: IFetchArgs) => UserRequests.detailsByUsername(args.id!), USER_DETAILS_BY_ID: (args: IFetchArgs) => UserRequests.detailsById(args.id!), USER_DETAILS_BY_IDS_BULK: (args: IFetchArgs) => UserRequests.bulkDetailsByIds(args.ids!), diff --git a/src/enums/Data.ts b/src/enums/Data.ts index 1ea170ae..dba20539 100644 --- a/src/enums/Data.ts +++ b/src/enums/Data.ts @@ -9,4 +9,5 @@ export enum BaseType { TWEET = 'TWEET', USER = 'USER', LIST = 'LIST', + BOOKMARK_FOLDER = 'BOOKMARK_FOLDER', } diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 17a88168..36fb4697 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -45,6 +45,8 @@ export enum ResourceType { USER_AFFILIATES = 'USER_AFFILIATES', USER_ANALYTICS = 'USER_ANALYTICS', USER_BOOKMARKS = 'USER_BOOKMARKS', + USER_BOOKMARK_FOLDERS = 'USER_BOOKMARK_FOLDERS', + USER_BOOKMARK_FOLDER_TWEETS = 'USER_BOOKMARK_FOLDER_TWEETS', USER_DETAILS_BY_USERNAME = 'USER_DETAILS_BY_USERNAME', USER_DETAILS_BY_ID = 'USER_DETAILS_BY_ID', USER_DETAILS_BY_IDS_BULK = 'USER_DETAILS_BY_IDS_BULK', diff --git a/src/models/data/BookmarkFolder.ts b/src/models/data/BookmarkFolder.ts new file mode 100644 index 00000000..25a6af16 --- /dev/null +++ b/src/models/data/BookmarkFolder.ts @@ -0,0 +1,71 @@ +import { LogActions } from '../../enums/Logging'; +import { LogService } from '../../services/internal/LogService'; +import { IBookmarkFolder } from '../../types/data/BookmarkFolder'; +import { IBookmarkFolder as IRawBookmarkFolder } from '../../types/raw/base/BookmarkFolder'; + +/** + * The details of a single Bookmark Folder. + * + * @public + */ +export class BookmarkFolder implements IBookmarkFolder { + /** The raw bookmark folder details. */ + private readonly _raw: IRawBookmarkFolder; + + public id: string; + public name: string; + + /** + * @param folder - The raw bookmark folder details. + */ + public constructor(folder: IRawBookmarkFolder) { + this._raw = { ...folder }; + this.id = folder.id; + this.name = folder.name; + } + + /** The raw bookmark folder details. */ + public get raw(): IRawBookmarkFolder { + return { ...this._raw }; + } + + /** + * Extracts and deserializes bookmark folders from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized list of bookmark folders. + */ + public static list(response: NonNullable): BookmarkFolder[] { + const folders: BookmarkFolder[] = []; + + // Extract items from the response structure + const items = (response as any)?.data?.viewer?.user_results?.result?.bookmark_collections_slice?.items; + + if (!items || !Array.isArray(items)) { + return folders; + } + + // Deserialize valid folders + for (const item of items) { + if (item && item.id) { + // Logging + LogService.log(LogActions.DESERIALIZE, { id: item.id }); + + folders.push(new BookmarkFolder(item)); + } + } + + return folders; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IBookmarkFolder { + return { + id: this.id, + name: this.name, + }; + } +} diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index 8b46682c..8057cf58 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -5,8 +5,8 @@ import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; +import { BookmarkFolder } from './BookmarkFolder'; import { List } from './List'; - import { Notification } from './Notification'; import { Tweet } from './Tweet'; import { User } from './User'; @@ -18,7 +18,7 @@ import { User } from './User'; * * @public */ -export class CursoredData implements ICursoredData { +export class CursoredData implements ICursoredData { public list: T[]; public next: string; @@ -43,6 +43,10 @@ export class CursoredData implemen } else if (type == BaseType.NOTIFICATION) { this.list = Notification.list(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; + } else if (type == BaseType.BOOKMARK_FOLDER) { + this.list = BookmarkFolder.list(response) as T[]; + const sliceInfo = (response as any)?.data?.viewer?.user_results?.result?.bookmark_collections_slice?.slice_info; + this.next = sliceInfo?.next_cursor ?? ''; } } diff --git a/src/requests/User.ts b/src/requests/User.ts index 4670c25d..ad143765 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -149,6 +149,126 @@ export class UserRequests { responsive_web_grok_analysis_button_from_backend: true, creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Fetches the list of bookmark folders for the logged-in user. + * + * @param cursor - The cursor to the batch of bookmark folders to fetch. + */ + public static bookmarkFolders(cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/i78YDd0Tza-dV4SYs58kRg/BookmarkFoldersSlice', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + cursor: cursor, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: false, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Fetches tweets from a specific bookmark folder. + * + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch. + * @param cursor - The cursor to the batch of tweets to fetch. + */ + public static bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/KJIQpsvxrTfRIlbaRIySHQ/BookmarkFolderTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + bookmark_collection_id: folderId, + count: count, + cursor: cursor, + includePromotedContent: true, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: false, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, longform_notetweets_rich_text_read_enabled: true, diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 0465070d..a985fdcf 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -3,6 +3,7 @@ import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Ana import { ResourceType } from '../../enums/Resource'; import { ProfileUpdateOptions } from '../../models/args/ProfileArgs'; import { Analytics } from '../../models/data/Analytics'; +import { BookmarkFolder } from '../../models/data/BookmarkFolder'; import { CursoredData } from '../../models/data/CursoredData'; import { List } from '../../models/data/List'; import { Notification } from '../../models/data/Notification'; @@ -13,6 +14,8 @@ import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates'; import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics'; import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; +import { IUserBookmarkFolderTweetsResponse } from '../../types/raw/user/BookmarkFolderTweets'; import { IUserDetailsResponse } from '../../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../../types/raw/user/DetailsBulk'; import { IUserFollowResponse } from '../../types/raw/user/Follow'; @@ -190,6 +193,88 @@ export class UserService extends FetcherService { return data; } + /** + * Get the list of bookmark folders of the logged in user. + * + * @param cursor - The cursor to the batch of bookmark folders to fetch. + * + * @returns The list of bookmark folders. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching all bookmark folders of the logged in user + * rettiwt.user.bookmarkFolders() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmarkFolders(cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARK_FOLDERS; + + // Fetching raw list of bookmark folders + const response = await this.request(resource, { + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the list of tweets in a specific bookmark folder of the logged in user. + * + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of tweets to fetch. + * + * @returns The list of tweets in the bookmark folder. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the first 100 tweets from bookmark folder with ID '2001752149647049173' + * rettiwt.user.bookmarkFolderTweets('2001752149647049173') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARK_FOLDER_TWEETS; + + // Fetching raw list of tweets from folder + const response = await this.request(resource, { + id: folderId, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + /** * Get the details of the logged in user. * diff --git a/src/types/data/BookmarkFolder.ts b/src/types/data/BookmarkFolder.ts new file mode 100644 index 00000000..3f80d5a7 --- /dev/null +++ b/src/types/data/BookmarkFolder.ts @@ -0,0 +1,12 @@ +/** + * The details of a single Bookmark Folder. + * + * @public + */ +export interface IBookmarkFolder { + /** The unique identifier for the folder. */ + id: string; + + /** The display name of the folder. */ + name: string; +} diff --git a/src/types/data/CursoredData.ts b/src/types/data/CursoredData.ts index 34d0c7e0..0c4a56f0 100644 --- a/src/types/data/CursoredData.ts +++ b/src/types/data/CursoredData.ts @@ -1,3 +1,4 @@ +import { IBookmarkFolder } from './BookmarkFolder'; import { IConversation } from './Conversation'; import { IDirectMessage } from './DirectMessage'; import { IList } from './List'; @@ -12,7 +13,7 @@ import { IUser } from './User'; * * @public */ -export interface ICursoredData { +export interface ICursoredData { /** The batch of data of the given type. */ list: T[]; diff --git a/src/types/raw/base/BookmarkFolder.ts b/src/types/raw/base/BookmarkFolder.ts new file mode 100644 index 00000000..32ff15d3 --- /dev/null +++ b/src/types/raw/base/BookmarkFolder.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * Represents the raw data of a single BookmarkFolder. + * + * @public + */ +export interface IBookmarkFolder { + /** The unique identifier for the folder. */ + id: string; + + /** The display name of the folder. */ + name: string; +} diff --git a/src/types/raw/user/BookmarkFolderTweets.ts b/src/types/raw/user/BookmarkFolderTweets.ts new file mode 100644 index 00000000..01e13121 --- /dev/null +++ b/src/types/raw/user/BookmarkFolderTweets.ts @@ -0,0 +1,53 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching tweets from a bookmark folder. + * + * @public + */ +export interface IUserBookmarkFolderTweetsResponse { + data: Data; +} + +interface Data { + bookmark_collection_timeline: BookmarkCollectionTimeline; +} + +interface BookmarkCollectionTimeline { + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; +} + +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + itemContent?: ItemContent; + value?: string; + cursorType?: string; + stopOnEmptyResponse?: boolean; +} + +interface ItemContent { + itemType: string; + __typename: string; + tweet_results: TweetResults; + tweetDisplayType: string; +} + +interface TweetResults { + result: any; // Uses the same tweet structure as regular bookmarks +} diff --git a/src/types/raw/user/BookmarkFolders.ts b/src/types/raw/user/BookmarkFolders.ts new file mode 100644 index 00000000..870d69b9 --- /dev/null +++ b/src/types/raw/user/BookmarkFolders.ts @@ -0,0 +1,41 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching bookmark folders. + * + * @public + */ +export interface IUserBookmarkFoldersResponse { + data: Data; +} + +interface Data { + viewer: Viewer; +} + +interface Viewer { + user_results: UserResults; +} + +interface UserResults { + result: Result; +} + +interface Result { + __typename: string; + bookmark_collections_slice: BookmarkCollectionsSlice; +} + +interface BookmarkCollectionsSlice { + items: BookmarkCollectionItem[]; + slice_info: SliceInfo; +} + +interface BookmarkCollectionItem { + id: string; + name: string; +} + +interface SliceInfo { + next_cursor?: string; +} From cec1c4024b089697bbe3d17185ce5f26fc117f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cavalheiro?= Date: Tue, 23 Dec 2025 19:23:01 +0000 Subject: [PATCH 113/119] Bookmark folders clean-up: Linting, formatting, documentation, etc --- README.md | 4 +++ src/collections/Extractors.ts | 2 +- src/collections/Requests.ts | 3 +- src/commands/User.ts | 30 ++++++++++++++++++ src/index.ts | 5 +++ src/models/data/BookmarkFolder.ts | 4 ++- src/models/data/CursoredData.ts | 4 ++- src/requests/User.ts | 28 ++++++++--------- src/services/public/UserService.ts | 46 ++++++++++++++-------------- src/types/data/CursoredData.ts | 4 ++- src/types/raw/base/BookmarkFolder.ts | 2 -- 11 files changed, 88 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b19746fe..46746a5c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Affiliates - User Analytics (Only for Premium accounts) - User Bookmarks + - User Bookmark Folders + - User Bookmark Folder Tweets - User Details - Single (by ID and Username) and Bulk (by ID only) - User Follow - User Followed Feed @@ -485,6 +487,8 @@ So far, the following operations are supported: - [Getting the list of users affiliated with the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#affiliates) - [Getting the analytics of the logged-in user (premium accounts only)](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#analytics) - [Getting the list of tweets bookmarked by the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarks) +- [Getting the list of bookmark folders of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarkFolders) +- [Getting the list of tweets in a specific bookmark folder](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarkFolderTweets) - [Getting the details of a user/multiple users](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#details) - [Following a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#follow) - [Getting the followed feed of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#followed) diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 5d9ba7a2..4a40b39a 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -35,9 +35,9 @@ import { ITweetUnretweetResponse } from '../types/raw/tweet/Unretweet'; import { ITweetUnscheduleResponse } from '../types/raw/tweet/Unschedule'; import { IUserAffiliatesResponse } from '../types/raw/user/Affiliates'; import { IUserAnalyticsResponse } from '../types/raw/user/Analytics'; -import { IUserBookmarksResponse } from '../types/raw/user/Bookmarks'; import { IUserBookmarkFoldersResponse } from '../types/raw/user/BookmarkFolders'; import { IUserBookmarkFolderTweetsResponse } from '../types/raw/user/BookmarkFolderTweets'; +import { IUserBookmarksResponse } from '../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../types/raw/user/DetailsBulk'; import { IUserFollowResponse } from '../types/raw/user/Follow'; diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 3cc1ce4d..346dc825 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -64,7 +64,8 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | ), USER_BOOKMARKS: (args: IFetchArgs) => UserRequests.bookmarks(args.count, args.cursor), USER_BOOKMARK_FOLDERS: (args: IFetchArgs) => UserRequests.bookmarkFolders(args.cursor), - USER_BOOKMARK_FOLDER_TWEETS: (args: IFetchArgs) => UserRequests.bookmarkFolderTweets(args.id!, args.count, args.cursor), + USER_BOOKMARK_FOLDER_TWEETS: (args: IFetchArgs) => + UserRequests.bookmarkFolderTweets(args.id!, args.count, args.cursor), USER_DETAILS_BY_USERNAME: (args: IFetchArgs) => UserRequests.detailsByUsername(args.id!), USER_DETAILS_BY_ID: (args: IFetchArgs) => UserRequests.detailsById(args.id!), USER_DETAILS_BY_IDS_BULK: (args: IFetchArgs) => UserRequests.bulkDetailsByIds(args.ids!), diff --git a/src/commands/User.ts b/src/commands/User.ts index 0f900f48..0c42a657 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -80,6 +80,36 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + user.command('bookmark-folders') + .description('Fetch your list of bookmark folders') + .argument('[cursor]', 'The cursor to the batch of bookmark folders to fetch') + .action(async (cursor?: string) => { + try { + const folders = await rettiwt.user.bookmarkFolders(cursor); + output(folders); + } catch (error) { + output(error); + } + }); + + user.command('bookmark-folder-tweets') + .description('Fetch tweets from a specific bookmark folder') + .argument('', 'The ID of the bookmark folder') + .argument('[count]', 'The number of tweets to fetch') + .argument('[cursor]', 'The cursor to the batch of tweets to fetch') + .action(async (folderId: string, count?: string, cursor?: string) => { + try { + const tweets = await rettiwt.user.bookmarkFolderTweets( + folderId, + count ? parseInt(count) : undefined, + cursor, + ); + output(tweets); + } catch (error) { + output(error); + } + }); + // Details user.command('details') .description('Fetch the details of the user with the given id/username') diff --git a/src/index.ts b/src/index.ts index 3889dfc2..8192848d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export * from './enums/Tweet'; export * from './models/args/FetchArgs'; export * from './models/args/PostArgs'; export * from './models/args/ProfileArgs'; +export * from './models/data/BookmarkFolder'; export * from './models/data/Conversation'; export * from './models/data/CursoredData'; export * from './models/data/DirectMessage'; @@ -47,6 +48,7 @@ export * from './services/public/UserService'; export * from './types/args/FetchArgs'; export * from './types/args/PostArgs'; export * from './types/args/ProfileArgs'; +export * from './types/data/BookmarkFolder'; export * from './types/data/Conversation'; export * from './types/data/CursoredData'; export * from './types/data/DirectMessage'; @@ -58,6 +60,7 @@ export * from './types/data/User'; export * from './types/errors/TwitterError'; export * from './types/params/Variables'; export { IAnalytics as IRawAnalytics } from './types/raw/base/Analytic'; +export { IBookmarkFolder as IRawBookmarkFolder } from './types/raw/base/BookmarkFolder'; export { ICursor as IRawCursor } from './types/raw/base/Cursor'; export { IErrorData as IRawErrorData, IErrorDetails as IRawErrorDetails } from './types/raw/base/Error'; export { ILimitedVisibilityTweet as IRawLimitedVisibilityTweet } from './types/raw/base/LimitedVisibilityTweet'; @@ -96,6 +99,8 @@ export { ITweetUnretweetResponse as IRawTweetUnretweetResponse } from './types/r export { ITweetUnscheduleResponse as ITRawTweetUnscheduleResponse } from './types/raw/tweet/Unschedule'; export { IUserAffiliatesResponse as IRawUserAffiliatesResponse } from './types/raw/user/Affiliates'; export { IUserAnalyticsResponse as IRawUserAnalyticsResponse } from './types/raw/user/Analytics'; +export { IUserBookmarkFoldersResponse as IRawUserBookmarkFoldersResponse } from './types/raw/user/BookmarkFolders'; +export { IUserBookmarkFolderTweetsResponse as IRawUserBookmarkFolderTweetsResponse } from './types/raw/user/BookmarkFolderTweets'; export { IUserBookmarksResponse as IRawUserBookmarksResponse } from './types/raw/user/Bookmarks'; export { IUserDetailsResponse as IRawUserDetailsResponse } from './types/raw/user/Details'; export { IUserDetailsBulkResponse as IRawUserDetailsBulkResponse } from './types/raw/user/DetailsBulk'; diff --git a/src/models/data/BookmarkFolder.ts b/src/models/data/BookmarkFolder.ts index 25a6af16..bff19bfd 100644 --- a/src/models/data/BookmarkFolder.ts +++ b/src/models/data/BookmarkFolder.ts @@ -2,6 +2,7 @@ import { LogActions } from '../../enums/Logging'; import { LogService } from '../../services/internal/LogService'; import { IBookmarkFolder } from '../../types/data/BookmarkFolder'; import { IBookmarkFolder as IRawBookmarkFolder } from '../../types/raw/base/BookmarkFolder'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; /** * The details of a single Bookmark Folder. @@ -40,7 +41,8 @@ export class BookmarkFolder implements IBookmarkFolder { const folders: BookmarkFolder[] = []; // Extract items from the response structure - const items = (response as any)?.data?.viewer?.user_results?.result?.bookmark_collections_slice?.items; + const items = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result + ?.bookmark_collections_slice?.items; if (!items || !Array.isArray(items)) { return folders; diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index 8057cf58..f19dc221 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -4,6 +4,7 @@ import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; import { BookmarkFolder } from './BookmarkFolder'; import { List } from './List'; @@ -45,7 +46,8 @@ export class CursoredData(response, 'cursorType', 'Bottom')[0]?.value ?? ''; } else if (type == BaseType.BOOKMARK_FOLDER) { this.list = BookmarkFolder.list(response) as T[]; - const sliceInfo = (response as any)?.data?.viewer?.user_results?.result?.bookmark_collections_slice?.slice_info; + const sliceInfo = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result + ?.bookmark_collections_slice?.slice_info; this.next = sliceInfo?.next_cursor ?? ''; } } diff --git a/src/requests/User.ts b/src/requests/User.ts index ad143765..0c496cdb 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -108,19 +108,23 @@ export class UserRequests { } /** - * @param count - The number of bookmarks to fetch. - * @param cursor - The cursor to the batch of bookmarks to fetch. + * Fetches tweets from a specific bookmark folder. + * + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch. + * @param cursor - The cursor to the batch of tweets to fetch. */ - public static bookmarks(count?: number, cursor?: string): AxiosRequestConfig { + public static bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/E6jlrZG4703s0mcA9DfNKQ/Bookmarks', + url: 'https://x.com/i/api/graphql/KJIQpsvxrTfRIlbaRIySHQ/BookmarkFolderTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ + bookmark_collection_id: folderId, count: count, cursor: cursor, - includePromotedContent: false, + includePromotedContent: true, }), features: JSON.stringify({ rweb_video_screen_enabled: false, @@ -222,23 +226,19 @@ export class UserRequests { } /** - * Fetches tweets from a specific bookmark folder. - * - * @param folderId - The ID of the bookmark folder. - * @param count - The number of tweets to fetch. - * @param cursor - The cursor to the batch of tweets to fetch. + * @param count - The number of bookmarks to fetch. + * @param cursor - The cursor to the batch of bookmarks to fetch. */ - public static bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): AxiosRequestConfig { + public static bookmarks(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/KJIQpsvxrTfRIlbaRIySHQ/BookmarkFolderTimeline', + url: 'https://x.com/i/api/graphql/-LGfdImKeQz0xS_jjUwzlA/Bookmarks', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ - bookmark_collection_id: folderId, count: count, cursor: cursor, - includePromotedContent: true, + includePromotedContent: false, }), features: JSON.stringify({ rweb_video_screen_enabled: false, diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index a985fdcf..8bf521ec 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -13,9 +13,9 @@ import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates'; import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics'; -import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; import { IUserBookmarkFolderTweetsResponse } from '../../types/raw/user/BookmarkFolderTweets'; +import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../../types/raw/user/DetailsBulk'; import { IUserFollowResponse } from '../../types/raw/user/Follow'; @@ -153,12 +153,13 @@ export class UserService extends FetcherService { } /** - * Get the list of bookmarks of the logged in user. + * Get the list of tweets in a specific bookmark folder of the logged in user. * - * @param count - The number of bookmakrs to fetch, must be \<= 100. - * @param cursor - The cursor to the batch of bookmarks to fetch. + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of tweets to fetch. * - * @returns The list of tweets bookmarked by the target user. + * @returns The list of tweets in the bookmark folder. * * @example * @@ -168,8 +169,8 @@ export class UserService extends FetcherService { * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * - * // Fetching the most recent 100 liked Tweets of the logged in User - * rettiwt.user.bookmarks() + * // Fetching the first 100 tweets from bookmark folder with ID '2001752149647049173' + * rettiwt.user.bookmarkFolderTweets('2001752149647049173') * .then(res => { * console.log(res); * }) @@ -178,11 +179,12 @@ export class UserService extends FetcherService { * }); * ``` */ - public async bookmarks(count?: number, cursor?: string): Promise> { - const resource = ResourceType.USER_BOOKMARKS; + public async bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARK_FOLDER_TWEETS; - // Fetching raw list of likes - const response = await this.request(resource, { + // Fetching raw list of tweets from folder + const response = await this.request(resource, { + id: folderId, count: count, cursor: cursor, }); @@ -233,13 +235,12 @@ export class UserService extends FetcherService { } /** - * Get the list of tweets in a specific bookmark folder of the logged in user. + * Get the list of bookmarks of the logged in user. * - * @param folderId - The ID of the bookmark folder. - * @param count - The number of tweets to fetch, must be \<= 100. - * @param cursor - The cursor to the batch of tweets to fetch. + * @param count - The number of bookmakrs to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of bookmarks to fetch. * - * @returns The list of tweets in the bookmark folder. + * @returns The list of tweets bookmarked by the target user. * * @example * @@ -249,8 +250,8 @@ export class UserService extends FetcherService { * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * - * // Fetching the first 100 tweets from bookmark folder with ID '2001752149647049173' - * rettiwt.user.bookmarkFolderTweets('2001752149647049173') + * // Fetching the most recent 100 liked Tweets of the logged in User + * rettiwt.user.bookmarks() * .then(res => { * console.log(res); * }) @@ -259,12 +260,11 @@ export class UserService extends FetcherService { * }); * ``` */ - public async bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): Promise> { - const resource = ResourceType.USER_BOOKMARK_FOLDER_TWEETS; + public async bookmarks(count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARKS; - // Fetching raw list of tweets from folder - const response = await this.request(resource, { - id: folderId, + // Fetching raw list of likes + const response = await this.request(resource, { count: count, cursor: cursor, }); diff --git a/src/types/data/CursoredData.ts b/src/types/data/CursoredData.ts index 0c4a56f0..79254889 100644 --- a/src/types/data/CursoredData.ts +++ b/src/types/data/CursoredData.ts @@ -13,7 +13,9 @@ import { IUser } from './User'; * * @public */ -export interface ICursoredData { +export interface ICursoredData< + T extends IDirectMessage | IConversation | INotification | ITweet | IUser | IList | IBookmarkFolder, +> { /** The batch of data of the given type. */ list: T[]; diff --git a/src/types/raw/base/BookmarkFolder.ts b/src/types/raw/base/BookmarkFolder.ts index 32ff15d3..d3434652 100644 --- a/src/types/raw/base/BookmarkFolder.ts +++ b/src/types/raw/base/BookmarkFolder.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ - /** * Represents the raw data of a single BookmarkFolder. * From 5bf7fea7f27c385246cb5220ca1898359d2d60ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cavalheiro?= Date: Thu, 8 Jan 2026 23:55:09 +0000 Subject: [PATCH 114/119] Removed duplicated properties to pass CI --- src/requests/User.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/requests/User.ts b/src/requests/User.ts index 0c496cdb..ca327e97 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -155,7 +155,6 @@ export class UserRequests { freedom_of_speech_not_reach_fetch_enabled: true, responsive_web_grok_imagine_annotation_enabled: false, responsive_web_grok_community_note_auto_translation_is_enabled: false, - responsive_web_profile_redirect_enabled: false, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, longform_notetweets_rich_text_read_enabled: true, @@ -274,8 +273,6 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, - responsive_web_grok_imagine_annotation_enabled: true, - responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ From bebfc5917e4ba50fcad359dbd424266400b8b749 Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 9 Jan 2026 16:24:52 +0100 Subject: [PATCH 115/119] fix(playground): removing workspace in favor `file:../` --- package-lock.json | 53 ++--------------------------------------- package.json | 6 +---- playground/package.json | 2 +- 3 files changed, 4 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d175ad..f6cf7b4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,6 @@ "name": "rettiwt-api", "version": "6.2.0", "license": "ISC", - "workspaces": [ - "playground", - "src" - ], "dependencies": { "axios": "^1.8.4", "chalk": "^5.4.1", @@ -1697,18 +1693,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4122,31 +4106,6 @@ "node": ">=4" } }, - "node_modules/rettiwt-api": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/rettiwt-api/-/rettiwt-api-6.0.5.tgz", - "integrity": "sha512-YqFu6TXl58hCo+k8TGLca5/NM6nJJKCRYRhEgH67YtuPVrA6aFA71k+S7hVE+9rmzOKCBAEbFN/h1A/j2WazqA==", - "license": "ISC", - "dependencies": { - "axios": "^1.8.4", - "chalk": "^5.4.1", - "commander": "^11.1.0", - "cookiejar": "^2.1.4", - "https-proxy-agent": "^7.0.6", - "node-html-parser": "^7.0.1", - "x-client-transaction-id-glacier": "^1.0.0" - }, - "bin": { - "rettiwt": "dist/cli.js" - }, - "engines": { - "node": "^22.13.1" - } - }, - "node_modules/rettiwt-playground": { - "resolved": "playground", - "link": true - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5041,15 +5000,6 @@ "linkedom": "^0.18.9" } }, - "node_modules/x-client-transaction-id-glacier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/x-client-transaction-id-glacier/-/x-client-transaction-id-glacier-1.0.0.tgz", - "integrity": "sha512-LmRJHgLTkksatezztTO+52SpGcwYLf90O2r2t4L9leAlhM5lOv/03c5izupwswmQAFA4Uk0str4qEV34KhBUAw==", - "license": "MIT", - "dependencies": { - "linkedom": "^0.18.9" - } - }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -5094,9 +5044,10 @@ "playground": { "name": "rettiwt-playground", "version": "1.0.0", + "extraneous": true, "dependencies": { "dotenv": "^17.2.0", - "rettiwt-api": "workspace" + "rettiwt-api": "file:../" } } } diff --git a/package.json b/package.json index 56909735..4aedface 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,5 @@ "jsdom": "^27.2.0", "node-html-parser": "^7.0.1", "x-client-transaction-id": "^0.1.9" - }, - "workspaces": [ - "playground", - "src" - ] + } } diff --git a/playground/package.json b/playground/package.json index 69b6f957..1d7a00b4 100644 --- a/playground/package.json +++ b/playground/package.json @@ -10,6 +10,6 @@ }, "dependencies": { "dotenv": "^17.2.0", - "rettiwt-api": "workspace" + "rettiwt-api": "file:../" } } From da111d7a95a6be99d643fa2cdbb33b7922d16c5d Mon Sep 17 00:00:00 2001 From: Valentin Chmara Date: Fri, 9 Jan 2026 16:26:17 +0100 Subject: [PATCH 116/119] docs(playground): use API_KEY instead of ACCESS_KEY --- playground/.env.example | 2 +- playground/README.md | 2 +- playground/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/.env.example b/playground/.env.example index f2fc89ed..f5e6c1a3 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -1 +1 @@ -ACCESS_TOKEN="" \ No newline at end of file +API_KEY="" \ No newline at end of file diff --git a/playground/README.md b/playground/README.md index b492eeb0..2c132cc5 100644 --- a/playground/README.md +++ b/playground/README.md @@ -23,7 +23,7 @@ This playground is intended for developers to test and experiment with features 2. **Environment Variables** Create a `.env` file in the `playground` directory with your API credentials: ```env - ACCESS_TOKEN=your_access_token_here + API_KEY=your_api_key_here ``` ### Usage diff --git a/playground/index.js b/playground/index.js index 791916cb..2f51031f 100644 --- a/playground/index.js +++ b/playground/index.js @@ -1,7 +1,7 @@ import { Rettiwt } from 'rettiwt-api'; import 'dotenv/config'; -const rettiwt = new Rettiwt({ apiKey: process.env.ACCESS_TOKEN }); +const rettiwt = new Rettiwt({ apiKey: process.env.API_KEY }); async function userDetails() { try { From e92d36fc7fde291cb708e5d9cc2ce5dacaaa9e76 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 12 Jan 2026 13:00:34 +0000 Subject: [PATCH 117/119] Updated README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 46746a5c..64f24b85 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,34 @@ Where, - `` is the username associated with the Twitter account. - `` is the password to the Twitter account. +## Using a custom error handler + +Out of the box, `Rettiwt`'s error handling is bare-minimum, only able to parse basic error messages. For advanced scenarios, where full error response might be required, in order to diagnose error reason, it's recommended to use a custom error handler, by implementing the `IErrorHandler` interface, as follows: + +```ts +import { Rettiwt, IErrorHandler } from 'rettiwt-api'; + +// Implementing an error handler +class CustomErrorHandler implements IErrorHandler { + /** + * This is where you handle the error yourself. + */ + public handler(error: unknown): void { + // The 'error' variable has the full, raw error response returned from Twitter. + /** + * You custom error handling logic goes here + */ + + console.log(`Raw Twitter Error: ${JSON.stringify(error)}`); + } +} + +// Now we'll use the implemented error handler while initializing Rettiwt +const rettiwt = new Rettiwt({ apiKey: '', errorHandler: CustomErrorHandler }); +``` + +You can then use the created `rettiwt` instance and your custom error handler will handler all the error responses, bypassing `Rettiwt`'s error handling logic. + ## Using a proxy For masking of IP address using a proxy server, use the following code snippet for instantiation of Rettiwt: From 5461b9e42c3af75b42593c3d2176515de3a11a80 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 19 Jan 2026 13:33:26 +0000 Subject: [PATCH 118/119] Added ability to search for users --- README.md | 2 + src/collections/Extractors.ts | 2 + src/collections/Groups.ts | 1 + src/collections/Requests.ts | 1 + src/commands/User.ts | 15 ++ src/enums/Resource.ts | 1 + src/index.ts | 1 + src/requests/User.ts | 61 ++++++++ src/services/public/UserService.ts | 44 ++++++ src/types/args/FetchArgs.ts | 2 +- src/types/raw/user/Search.ts | 230 +++++++++++++++++++++++++++++ 11 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/types/raw/user/Search.ts diff --git a/README.md b/README.md index 64f24b85..7e1191f2 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Notification - User Recommended Feed - User Replies Timeline + - User Search - User Subscriptions - User Timeline - User Unfollow @@ -529,6 +530,7 @@ So far, the following operations are supported: - [Streaming notifications of the logged-in user in pseudo-realtime](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#notifications) - [Getting the recommended feed of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#recommended) - [Getting the replies timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#replies) +- [Searching for a username](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#search) - [Getting the tweet timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#timeline) - [Unfollowing a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#unfollow) - [Updating the profile of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#updateProfile) diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index 4a40b39a..b18b21f3 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -51,6 +51,7 @@ import { IUserMediaResponse } from '../types/raw/user/Media'; import { IUserNotificationsResponse } from '../types/raw/user/Notifications'; import { IUserProfileUpdateResponse } from '../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../types/raw/user/Recommended'; +import { IUserSearchResponse } from '../types/raw/user/Search'; import { IUserSubscriptionsResponse } from '../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../types/raw/user/TweetsAndReplies'; @@ -140,6 +141,7 @@ export const Extractors = { new CursoredData(response, BaseType.TWEET), USER_NOTIFICATIONS: (response: IUserNotificationsResponse): CursoredData => new CursoredData(response, BaseType.NOTIFICATION), + USER_SEARCH: (response: IUserSearchResponse): CursoredData => new CursoredData(response, BaseType.USER), USER_SUBSCRIPTIONS: (response: IUserSubscriptionsResponse): CursoredData => new CursoredData(response, BaseType.USER), USER_TIMELINE: (response: IUserTweetsResponse): CursoredData => diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index 050adc76..6cf0229d 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -47,6 +47,7 @@ export const FetchResourcesGroup = [ ResourceType.USER_LISTS, ResourceType.USER_MEDIA, ResourceType.USER_NOTIFICATIONS, + ResourceType.USER_SEARCH, ResourceType.USER_SUBSCRIPTIONS, ResourceType.USER_TIMELINE, ResourceType.USER_TIMELINE_AND_REPLIES, diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 346dc825..cdd7b969 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -79,6 +79,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | USER_LISTS: (args: IFetchArgs) => UserRequests.lists(args.id!, args.count, args.cursor), USER_MEDIA: (args: IFetchArgs) => UserRequests.media(args.id!, args.count, args.cursor), USER_NOTIFICATIONS: (args: IFetchArgs) => UserRequests.notifications(args.count, args.cursor), + USER_SEARCH: (args: IFetchArgs) => UserRequests.search(args.id!, args.count, args.cursor), USER_SUBSCRIPTIONS: (args: IFetchArgs) => UserRequests.subscriptions(args.id!, args.count, args.cursor), USER_TIMELINE: (args: IFetchArgs) => UserRequests.tweets(args.id!, args.count, args.cursor), USER_TIMELINE_AND_REPLIES: (args: IFetchArgs) => UserRequests.tweetsAndReplies(args.id!, args.count, args.cursor), diff --git a/src/commands/User.ts b/src/commands/User.ts index 0c42a657..91c1ef5a 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -276,6 +276,21 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Replies + user.command('search') + .description('Search for a username') + .argument('', 'The username to search for') + .argument('[count]', 'The number of results to fetch') + .argument('[cursor]', 'The cursor to the batch of results to fetch') + .action(async (userName: string, count?: string, cursor?: string) => { + try { + const replies = await rettiwt.user.search(userName, count ? parseInt(count) : undefined, cursor); + output(replies); + } catch (error) { + output(error); + } + }); + // Timeline user.command('timeline') .description('Fetch the tweets timeline the given user') diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 36fb4697..24b36aad 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -60,6 +60,7 @@ export enum ResourceType { USER_LISTS = 'USER_LISTS', USER_MEDIA = 'USER_MEDIA', USER_NOTIFICATIONS = 'USER_NOTIFICATIONS', + USER_SEARCH = 'USER_SEARCH', USER_SUBSCRIPTIONS = 'USER_SUBSCRIPTIONS', USER_TIMELINE = 'USER_TIMELINE', USER_TIMELINE_AND_REPLIES = 'USER_TIMELINE_AND_REPLIES', diff --git a/src/index.ts b/src/index.ts index 8192848d..2e5f4b74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,6 +113,7 @@ export { IUserLikesResponse as IRawUserLikesResponse } from './types/raw/user/Li export { IUserMediaResponse as IRawUserMediaResponse } from './types/raw/user/Media'; export { IUserNotificationsResponse as IRawUserNotificationsResponse } from './types/raw/user/Notifications'; export { IUserRecommendedResponse as IRawUserRecommendedResponse } from './types/raw/user/Recommended'; +export { IUserSearchResponse as IRawUserSearchResponse } from './types/raw/user/Search'; export { IUserScheduledResponse as IRawUserScheduledResponse } from './types/raw/user/Scheduled'; export { IUserSubscriptionsResponse as IRawUserSubscriptionsResponse } from './types/raw/user/Subscriptions'; export { IUserTweetsResponse as IRawUserTweetsResponse } from './types/raw/user/Tweets'; diff --git a/src/requests/User.ts b/src/requests/User.ts index ca327e97..f719fcca 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -933,6 +933,67 @@ export class UserRequests { }; } + /** + * @param userName - The username to search for. + * @param count - The number of user matches to fetch. Only works as a lower limit when used with a cursor. + * @param cursor - The cursor to the batch of results to fetch. + */ + public static search(userName: string, count?: number, cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/M1jEez78PEfVfbQLvlWMvQ/SearchTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + rawQuery: userName, + count: count, + cursor: cursor, + querySource: 'typed_query', + product: 'People', + withGrokTranslatedBio: false, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + /** * @param id - The id of the user whose subscriptions are to be fetched. * @param count - The number of subscriptions to fetch. Only works as a lower limit when used with a cursor. diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 8bf521ec..ca3bb714 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -29,6 +29,7 @@ import { IUserMediaResponse } from '../../types/raw/user/Media'; import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; import { IUserProfileUpdateResponse } from '../../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../../types/raw/user/Recommended'; +import { IUserSearchResponse } from '../../types/raw/user/Search'; import { IUserSubscriptionsResponse } from '../../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../../types/raw/user/TweetsAndReplies'; @@ -915,6 +916,49 @@ export class UserService extends FetcherService { return data; } + /** + * Search for a username. + * + * @param userName - The username to search for. + * @param count - The number of results to fetch, must be \<= 20. + * @param cursor - The cursor to the batch of results to fetch. + * + * @returns The list of users that match the given username. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the top 5 matching users for the username 'user1' + * rettiwt.user.search('user1') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async search(userName: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_SEARCH; + + // Fetching raw list of filtered tweets + const response = await this.request(resource, { + id: userName, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + /** * Get the list of subscriptions of a user. * diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index d21dd683..318f787c 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -71,7 +71,7 @@ export interface IFetchArgs { * * @remarks * - Required for all resources except {@link ResourceType.TWEET_SEARCH} and {@link ResourceType.USER_TIMELINE_RECOMMENDED}. - * - For {@link ResourceType.USER_DETAILS_BY_USERNAME}, can be alphanumeric, while for others, is strictly numeric. + * - For {@link ResourceType.USER_DETAILS_BY_USERNAME} and {@link ResourceType.USER_SEARCH}, can be alphanumeric, while for others, is strictly numeric. */ id?: string; diff --git a/src/types/raw/user/Search.ts b/src/types/raw/user/Search.ts new file mode 100644 index 00000000..8efecd1c --- /dev/null +++ b/src/types/raw/user/Search.ts @@ -0,0 +1,230 @@ +/* eslint-disable */ + +/** + * The raw data received when search for users. + * + * @public + */ +export interface IUserSearchResponse { + data: Data; +} + +interface Data { + search_by_raw_query: SearchByRawQuery; +} + +interface SearchByRawQuery { + search_timeline: SearchTimeline; +} + +interface SearchTimeline { + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; +} + +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + itemContent?: ItemContent; + clientEventInfo?: ClientEventInfo; + value?: string; + cursorType?: string; +} + +interface ItemContent { + itemType: string; + __typename: string; + user_results: UserResults; + userDisplayType: string; +} + +interface UserResults { + result: Result; +} + +interface Result { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + professional?: Professional; + profile_bio: ProfileBio; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + super_follow_eligible?: boolean; + verification: Verification; + verified_phone_status: boolean; + profile_description_language?: string; +} + +interface AffiliatesHighlightedLabel { + label?: Label; +} + +interface Label { + url: Url; + badge: Badge; + description: string; + userLabelType: string; + userLabelDisplayType: string; +} + +interface Url { + url: string; + urlType: string; +} + +interface Badge { + url: string; +} + +interface Avatar { + image_url: string; +} + +interface Core { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + pinned_tweet_ids_str: string[]; + possibly_sensitive: boolean; + profile_banner_url?: string; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; + url?: string; +} + +interface Entities { + description: Description; + url?: Url3; +} + +interface Description { + urls: Url2[]; +} + +interface Url2 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +interface Url3 { + urls: Url4[]; +} + +interface Url4 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface Professional { + rest_id: string; + professional_type: string; + category: Category[]; +} + +interface Category { + id: number; + name: string; + icon_name: string; +} + +interface ProfileBio { + description: string; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + following: boolean; +} + +interface TipjarSettings { + is_enabled?: boolean; + bitcoin_handle?: string; + ethereum_handle?: string; + cash_app_handle?: string; + venmo_handle?: string; +} + +interface Verification { + verified: boolean; + verified_type?: string; +} + +interface ClientEventInfo { + component: string; + element: string; + details: Details; +} + +interface Details { + timelinesDetails: TimelinesDetails; +} + +interface TimelinesDetails { + controllerData: string; +} From a6cd1f95d8b67c6d509cc58433a6cf71ae77e255 Mon Sep 17 00:00:00 2001 From: Rishikant Sahu Date: Mon, 19 Jan 2026 13:38:52 +0000 Subject: [PATCH 119/119] Bumped version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6cf7b4f..58ff6cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rettiwt-api", - "version": "6.2.0", + "version": "6.3.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "6.2.0", + "version": "6.3.0-alpha.0", "license": "ISC", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 4aedface..869a47fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "6.2.0", + "version": "6.3.0-alpha.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!",