Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two
- Tweet Unschedule
- User Affiliates
- User Analytics (Only for Premium accounts)
- User About Profile (by username)
- User Bookmarks
- User Bookmark Folders
- User Bookmark Folder Tweets
Expand Down Expand Up @@ -517,6 +518,7 @@ 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 about profile of a user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#about)
- [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)
Expand Down
3 changes: 3 additions & 0 deletions src/collections/Extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Notification } from '../models/data/Notification';
import { Space } from '../models/data/Space';
import { Tweet } from '../models/data/Tweet';
import { User } from '../models/data/User';
import { UserAbout } from '../models/data/UserAbout';
import { IConversationTimelineResponse } from '../types/raw/dm/Conversation';
import { IInboxInitialResponse } from '../types/raw/dm/InboxInitial';
import { IInboxTimelineResponse } from '../types/raw/dm/InboxTimeline';
Expand All @@ -35,6 +36,7 @@ import { ITweetUnlikeResponse } from '../types/raw/tweet/Unlike';
import { ITweetUnpostResponse } from '../types/raw/tweet/Unpost';
import { ITweetUnretweetResponse } from '../types/raw/tweet/Unretweet';
import { ITweetUnscheduleResponse } from '../types/raw/tweet/Unschedule';
import { IUserAboutResponse } from '../types/raw/user/About';
import { IUserAffiliatesResponse } from '../types/raw/user/Affiliates';
import { IUserAnalyticsResponse } from '../types/raw/user/Analytics';
import { IUserBookmarkFoldersResponse } from '../types/raw/user/BookmarkFolders';
Expand Down Expand Up @@ -123,6 +125,7 @@ export const Extractors = {
new CursoredData<BookmarkFolder>(response, BaseType.BOOKMARK_FOLDER),
USER_BOOKMARK_FOLDER_TWEETS: (response: IUserBookmarkFolderTweetsResponse): CursoredData<Tweet> =>
new CursoredData<Tweet>(response, BaseType.TWEET),
USER_ABOUT_BY_USERNAME: (response: IUserAboutResponse): UserAbout | undefined => UserAbout.single(response),
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[] =>
Expand Down
1 change: 1 addition & 0 deletions src/collections/Groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const FetchResourcesGroup = [
ResourceType.USER_BOOKMARKS,
ResourceType.USER_BOOKMARK_FOLDERS,
ResourceType.USER_BOOKMARK_FOLDER_TWEETS,
ResourceType.USER_ABOUT_BY_USERNAME,
ResourceType.USER_DETAILS_BY_USERNAME,
ResourceType.USER_DETAILS_BY_ID,
ResourceType.USER_DETAILS_BY_IDS_BULK,
Expand Down
1 change: 1 addition & 0 deletions src/collections/Requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs |
USER_BOOKMARK_FOLDERS: (args: IFetchArgs) => UserRequests.bookmarkFolders(args.cursor),
USER_BOOKMARK_FOLDER_TWEETS: (args: IFetchArgs) =>
UserRequests.bookmarkFolderTweets(args.id!, args.count, args.cursor),
USER_ABOUT_BY_USERNAME: (args: IFetchArgs) => UserRequests.aboutByUsername(args.id!),
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!),
Expand Down
13 changes: 13 additions & 0 deletions src/commands/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ function createUserCommand(rettiwt: Rettiwt): Command {
}
});

// About
user.command('about')
.description('Fetch the about profile of the user with the given username')
.argument('<username>', 'The username of the user')
.action(async (username: string) => {
try {
const about = await rettiwt.user.about(username);
output(about);
} catch (error) {
output(error);
}
});

// Details
user.command('details')
.description('Fetch the details of the user with the given id/username')
Expand Down
1 change: 1 addition & 0 deletions src/enums/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export enum ResourceType {
USER_BOOKMARKS = 'USER_BOOKMARKS',
USER_BOOKMARK_FOLDERS = 'USER_BOOKMARK_FOLDERS',
USER_BOOKMARK_FOLDER_TWEETS = 'USER_BOOKMARK_FOLDER_TWEETS',
USER_ABOUT_BY_USERNAME = 'USER_ABOUT_BY_USERNAME',
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',
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export * from './models/data/Notification';
export * from './models/data/Space';
export * from './models/data/Tweet';
export * from './models/data/User';
export * from './models/data/UserAbout';
export * from './models/errors/TwitterError';

// REQUESTS
Expand Down Expand Up @@ -61,6 +62,7 @@ export * from './types/data/Notification';
export * from './types/data/Space';
export * from './types/data/Tweet';
export * from './types/data/User';
export * from './types/data/UserAbout';
export * from './types/errors/TwitterError';
export * from './types/params/Variables';
export { IAnalytics as IRawAnalytics } from './types/raw/base/Analytic';
Expand Down Expand Up @@ -102,6 +104,7 @@ export { ITweetUnlikeResponse as IRawTweetUnlikeResponse } from './types/raw/twe
export { ITweetUnpostResponse as IRawTweetUnpostResponse } from './types/raw/tweet/Unpost';
export { ITweetUnretweetResponse as IRawTweetUnretweetResponse } from './types/raw/tweet/Unretweet';
export { ITweetUnscheduleResponse as ITRawTweetUnscheduleResponse } from './types/raw/tweet/Unschedule';
export { IUserAboutResponse as IRawUserAboutResponse } from './types/raw/user/About';
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';
Expand Down
161 changes: 161 additions & 0 deletions src/models/data/UserAbout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { LogActions } from '../../enums/Logging';
import { LogService } from '../../services/internal/LogService';
import {
IUserAbout,
IUserAboutProfile,
IUserAboutUsernameChanges,
IUserAboutVerificationInfo,
} from '../../types/data/UserAbout';
import { IUserAboutResponse, IUserAboutResult } from '../../types/raw/user/About';

/* eslint-disable @typescript-eslint/naming-convention */
type IRawUsernameChanges = {
count?: string;
last_changed_at_msec?: string;
};
/* eslint-enable @typescript-eslint/naming-convention */

/**
* The about profile details of a single user.
*
* @public
*/
export class UserAbout implements IUserAbout {
/** The raw about profile details. */
private readonly _raw: IUserAboutResult;

public aboutProfile?: IUserAboutProfile;
public createdAt: string;
public fullName: string;
public id: string;
public isProtected?: boolean;
public isVerified: boolean;
public profileImage: string;
public profileImageShape?: string;
public userName: string;
public verificationInfo?: IUserAboutVerificationInfo;

/**
* @param user - The raw about profile details.
*/
public constructor(user: IUserAboutResult) {
this._raw = { ...user };

this.id = user.rest_id ?? user.id ?? '';
this.userName = user.core?.screen_name ?? '';
this.fullName = user.core?.name ?? '';
this.createdAt = new Date(user.core?.created_at ?? 0).toISOString();
this.profileImage = user.avatar?.image_url ?? '';
this.profileImageShape = user.profile_image_shape;
this.isVerified = user.is_blue_verified ?? false;
this.isProtected = user.privacy?.protected;
this.aboutProfile = UserAbout._buildAboutProfile(user);
this.verificationInfo = UserAbout._buildVerificationInfo(user);
}

/** The raw about profile details. */
public get raw(): IUserAboutResult {
return { ...this._raw };
}

private static _buildAboutProfile(user: IUserAboutResult): IUserAboutProfile | undefined {
const profile = user.about_profile;

if (!profile) {
return undefined;
}

const usernameChanges = UserAbout._buildUsernameChanges(profile.username_changes);

return {
createdCountryAccurate: profile.created_country_accurate,
accountBasedIn: profile.account_based_in,
locationAccurate: profile.location_accurate,
learnMoreUrl: profile.learn_more_url,
source: profile.source,
usernameChanges: usernameChanges,
};
}

private static _buildUsernameChanges(changes?: IRawUsernameChanges): IUserAboutUsernameChanges | undefined {
if (!changes) {
return undefined;
}

return {
count: UserAbout._toNumber(changes.count),
lastChangedAt: UserAbout._toIsoFromMsec(changes.last_changed_at_msec),
};
}

private static _buildVerificationInfo(user: IUserAboutResult): IUserAboutVerificationInfo | undefined {
const info = user.verification_info;

if (!info) {
return undefined;
}

return {
isIdentityVerified: info.is_identity_verified,
verifiedSince: UserAbout._toIsoFromMsec(info.reason?.verified_since_msec),
};
}

private static _toIsoFromMsec(value?: string | number): string | undefined {
const parsed = UserAbout._toNumber(value);

return parsed === undefined ? undefined : new Date(parsed).toISOString();
}

private static _toNumber(value?: string | number): number | undefined {
if (value === undefined || value === null) {
return undefined;
}

const parsed = typeof value === 'number' ? value : Number(value);

return Number.isFinite(parsed) ? parsed : undefined;
}

/**
* Extracts and deserializes a single target user about profile from the given raw response data.
*
* @param response - The raw response data.
*
* @returns The target deserialized user about profile.
*/
public static single(response: NonNullable<unknown>): UserAbout | undefined {
const result = (response as IUserAboutResponse)?.data?.user_result_by_screen_name?.result;

if (!result || result.__typename !== 'User') {
LogService.log(LogActions.WARNING, {
action: LogActions.DESERIALIZE,
message: `User not found, skipping`,
});
return undefined;
}

// Logging
LogService.log(LogActions.DESERIALIZE, { id: result.rest_id ?? result.id });

return new UserAbout(result);
}

/**
* @returns A serializable JSON representation of `this` object.
*/
public toJSON(): IUserAbout {
return {
id: this.id,
userName: this.userName,
fullName: this.fullName,
createdAt: this.createdAt,
profileImage: this.profileImage,
profileImageShape: this.profileImageShape,
isVerified: this.isVerified,
isProtected: this.isProtected,
aboutProfile: this.aboutProfile,
verificationInfo: this.verificationInfo,
};
}
}
14 changes: 14 additions & 0 deletions src/requests/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import { IProfileUpdateOptions } from '../types/args/ProfileArgs';
* @public
*/
export class UserRequests {
/**
* @param userName - The username of the user whose about profile is to be fetched.
*/
public static aboutByUsername(userName: string): AxiosRequestConfig {
return {
method: 'get',
url: 'https://x.com/i/api/graphql/zs_jFPFT78rBpXv9Z3U2YQ/AboutAccountQuery',
params: {
variables: JSON.stringify({ screenName: userName }),
},
paramsSerializer: { encode: encodeURIComponent },
};
}

/**
* @param id - The id of the user whose affiliates are to be fetched.
* @param count - The number of affiliates to fetch. Only works as a lower limit when used with a cursor.
Expand Down
43 changes: 43 additions & 0 deletions src/services/public/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { List } from '../../models/data/List';
import { Notification } from '../../models/data/Notification';
import { Tweet } from '../../models/data/Tweet';
import { User } from '../../models/data/User';
import { UserAbout } from '../../models/data/UserAbout';
import { RettiwtConfig } from '../../models/RettiwtConfig';
import { IProfileUpdateOptions } from '../../types/args/ProfileArgs';
import { IUserAboutResponse } from '../../types/raw/user/About';
import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates';
import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics';
import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders';
Expand Down Expand Up @@ -52,6 +54,47 @@ export class UserService extends FetcherService {
super(config);
}

/**
* Get the about profile of a user.
*
* @param userName - The username/screenname of the target user.
*
* @returns The about profile 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 about profile of the User with username 'user1' or '@user1'
* rettiwt.user.about('user1') // or @user1
* .then(res => {
* console.log(res);
* })
* .catch(err => {
* console.log(err);
* });
* ```
*/
public async about(userName: string): Promise<UserAbout | undefined> {
const resource = ResourceType.USER_ABOUT_BY_USERNAME;

if (userName.startsWith('@')) {
userName = userName.slice(1);
}

// Fetching raw about profile
const response = await this.request<IUserAboutResponse>(resource, { id: userName });

// Deserializing response
const data = Extractors[resource](response);

return data;
}

/**
* Get the list affiliates of a user.
*
Expand Down
2 changes: 1 addition & 1 deletion src/types/args/FetchArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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} and {@link ResourceType.USER_SEARCH}, can be alphanumeric, while for others, is strictly numeric.
* - For {@link ResourceType.USER_DETAILS_BY_USERNAME}, {@link ResourceType.USER_ABOUT_BY_USERNAME}, and {@link ResourceType.USER_SEARCH}, can be alphanumeric, while for others, is strictly numeric.
*/
id?: string;

Expand Down
Loading