@glagan/rettiwt-api 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +166 -0
- package/.gitattributes +3 -0
- package/.github/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yml +57 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +20 -0
- package/.github/ISSUE_TEMPLATE/question.yml +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +32 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/publish.yml +23 -0
- package/.nvmrc +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +13 -0
- package/LICENSE +21 -0
- package/README.md +566 -0
- package/dist/cli.js +43 -0
- package/eslint.config.mjs +17 -0
- package/package.json +50 -0
- package/src/Rettiwt.ts +97 -0
- package/src/cli.ts +48 -0
- package/src/collections/Extractors.ts +155 -0
- package/src/collections/Groups.ts +81 -0
- package/src/collections/Requests.ts +89 -0
- package/src/collections/Tweet.ts +17 -0
- package/src/commands/DirectMessage.ts +62 -0
- package/src/commands/List.ts +90 -0
- package/src/commands/Tweet.ts +437 -0
- package/src/commands/User.ts +367 -0
- package/src/enums/Api.ts +10 -0
- package/src/enums/Authentication.ts +10 -0
- package/src/enums/Data.ts +13 -0
- package/src/enums/Logging.ts +14 -0
- package/src/enums/Media.ts +10 -0
- package/src/enums/Notification.ts +12 -0
- package/src/enums/Resource.ts +69 -0
- package/src/enums/Tweet.ts +8 -0
- package/src/enums/raw/Analytics.ts +32 -0
- package/src/enums/raw/Media.ts +10 -0
- package/src/enums/raw/Notification.ts +11 -0
- package/src/enums/raw/Tweet.ts +20 -0
- package/src/helper/CliUtils.ts +17 -0
- package/src/helper/JsonUtils.ts +70 -0
- package/src/index.ts +128 -0
- package/src/models/RettiwtConfig.ts +101 -0
- package/src/models/args/FetchArgs.ts +169 -0
- package/src/models/args/PostArgs.ts +93 -0
- package/src/models/args/ProfileArgs.ts +68 -0
- package/src/models/auth/AuthCookie.ts +58 -0
- package/src/models/auth/AuthCredential.ts +83 -0
- package/src/models/data/Analytics.ts +97 -0
- package/src/models/data/BookmarkFolder.ts +73 -0
- package/src/models/data/Conversation.ts +344 -0
- package/src/models/data/CursoredData.ts +64 -0
- package/src/models/data/DirectMessage.ts +335 -0
- package/src/models/data/Inbox.ts +124 -0
- package/src/models/data/List.ts +113 -0
- package/src/models/data/Notification.ts +84 -0
- package/src/models/data/Tweet.ts +388 -0
- package/src/models/data/User.ts +187 -0
- package/src/models/errors/TwitterError.ts +65 -0
- package/src/models/params/Variables.ts +62 -0
- package/src/requests/DirectMessage.ts +229 -0
- package/src/requests/List.ts +203 -0
- package/src/requests/Media.ts +67 -0
- package/src/requests/Tweet.ts +607 -0
- package/src/requests/User.ts +1191 -0
- package/src/services/internal/AuthService.ts +115 -0
- package/src/services/internal/ErrorService.ts +41 -0
- package/src/services/internal/LogService.ts +34 -0
- package/src/services/public/DirectMessageService.ts +159 -0
- package/src/services/public/FetcherService.ts +366 -0
- package/src/services/public/ListService.ts +241 -0
- package/src/services/public/TweetService.ts +886 -0
- package/src/services/public/UserService.ts +1154 -0
- package/src/types/ErrorHandler.ts +13 -0
- package/src/types/Fetch.ts +3 -0
- package/src/types/RettiwtConfig.ts +48 -0
- package/src/types/args/FetchArgs.ts +233 -0
- package/src/types/args/PostArgs.ts +142 -0
- package/src/types/args/ProfileArgs.ts +33 -0
- package/src/types/auth/AuthCookie.ts +22 -0
- package/src/types/auth/AuthCredential.ts +28 -0
- package/src/types/auth/TransactionHeader.ts +8 -0
- package/src/types/data/Analytics.ts +58 -0
- package/src/types/data/BookmarkFolder.ts +12 -0
- package/src/types/data/Conversation.ts +44 -0
- package/src/types/data/CursoredData.ts +24 -0
- package/src/types/data/DirectMessage.ts +33 -0
- package/src/types/data/Inbox.ts +23 -0
- package/src/types/data/List.ts +33 -0
- package/src/types/data/Notification.ts +26 -0
- package/src/types/data/Tweet.ts +99 -0
- package/src/types/data/User.ts +54 -0
- package/src/types/errors/TwitterError.ts +37 -0
- package/src/types/params/Variables.ts +41 -0
- package/src/types/raw/base/Analytic.ts +32 -0
- package/src/types/raw/base/BookmarkFolder.ts +12 -0
- package/src/types/raw/base/Cursor.ts +13 -0
- package/src/types/raw/base/Error.ts +38 -0
- package/src/types/raw/base/LimitedVisibilityTweet.ts +40 -0
- package/src/types/raw/base/List.ts +50 -0
- package/src/types/raw/base/Media.ts +53 -0
- package/src/types/raw/base/Message.ts +22 -0
- package/src/types/raw/base/Notification.ts +66 -0
- package/src/types/raw/base/Space.ts +35 -0
- package/src/types/raw/base/Tweet.ts +139 -0
- package/src/types/raw/base/User.ts +182 -0
- package/src/types/raw/composite/DataResult.ts +8 -0
- package/src/types/raw/composite/TimelineList.ts +10 -0
- package/src/types/raw/composite/TimelineTweet.ts +14 -0
- package/src/types/raw/composite/TimelineUser.ts +13 -0
- package/src/types/raw/dm/Conversation.ts +59 -0
- package/src/types/raw/dm/InboxInitial.ts +155 -0
- package/src/types/raw/dm/InboxTimeline.ts +301 -0
- package/src/types/raw/dm/UserUpdates.ts +46 -0
- package/src/types/raw/generic/Response.ts +10 -0
- package/src/types/raw/list/AddMember.ts +175 -0
- package/src/types/raw/list/Details.ts +176 -0
- package/src/types/raw/list/Members.ts +154 -0
- package/src/types/raw/list/RemoveMember.ts +174 -0
- package/src/types/raw/list/Tweets.ts +2296 -0
- package/src/types/raw/media/FinalizeUpload.ts +20 -0
- package/src/types/raw/media/InitalizeUpload.ts +12 -0
- package/src/types/raw/media/LiveVideoStream.ts +21 -0
- package/src/types/raw/space/Details.ts +359 -0
- package/src/types/raw/tweet/Bookmark.ts +14 -0
- package/src/types/raw/tweet/Details.ts +210 -0
- package/src/types/raw/tweet/DetailsBulk.ts +338 -0
- package/src/types/raw/tweet/Like.ts +14 -0
- package/src/types/raw/tweet/Likers.ts +200 -0
- package/src/types/raw/tweet/Post.ts +150 -0
- package/src/types/raw/tweet/Replies.ts +539 -0
- package/src/types/raw/tweet/Retweet.ts +31 -0
- package/src/types/raw/tweet/Retweeters.ts +208 -0
- package/src/types/raw/tweet/Schedule.ts +18 -0
- package/src/types/raw/tweet/Search.ts +597 -0
- package/src/types/raw/tweet/Unbookmark.ts +14 -0
- package/src/types/raw/tweet/Unlike.ts +14 -0
- package/src/types/raw/tweet/Unpost.ts +20 -0
- package/src/types/raw/tweet/Unretweet.ts +31 -0
- package/src/types/raw/tweet/Unschedule.ts +14 -0
- package/src/types/raw/user/Affiliates.ts +179 -0
- package/src/types/raw/user/Analytics.ts +23 -0
- package/src/types/raw/user/BookmarkFolderTweets.ts +53 -0
- package/src/types/raw/user/BookmarkFolders.ts +41 -0
- package/src/types/raw/user/Bookmarks.ts +637 -0
- package/src/types/raw/user/Details.ts +185 -0
- package/src/types/raw/user/DetailsBulk.ts +104 -0
- package/src/types/raw/user/Follow.ts +280 -0
- package/src/types/raw/user/Followed.ts +1942 -0
- package/src/types/raw/user/Followers.ts +215 -0
- package/src/types/raw/user/Following.ts +215 -0
- package/src/types/raw/user/Highlights.ts +1287 -0
- package/src/types/raw/user/Likes.ts +1254 -0
- package/src/types/raw/user/Lists.ts +378 -0
- package/src/types/raw/user/Media.ts +1738 -0
- package/src/types/raw/user/Notifications.ts +499 -0
- package/src/types/raw/user/ProfileUpdate.ts +80 -0
- package/src/types/raw/user/Recommended.ts +2319 -0
- package/src/types/raw/user/Scheduled.ts +37 -0
- package/src/types/raw/user/Search.ts +230 -0
- package/src/types/raw/user/Subscriptions.ts +176 -0
- package/src/types/raw/user/Tweets.ts +1254 -0
- package/src/types/raw/user/TweetsAndReplies.ts +1254 -0
- package/src/types/raw/user/Unfollow.ts +280 -0
- package/tsconfig.json +97 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Cookie } from 'cookiejar';
|
|
2
|
+
|
|
3
|
+
import { AuthenticationType } from '../../enums/Authentication';
|
|
4
|
+
import { IAuthCredential } from '../../types/auth/AuthCredential';
|
|
5
|
+
|
|
6
|
+
import { AuthCookie } from './AuthCookie';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The credentials for authenticating against Twitter.
|
|
10
|
+
*
|
|
11
|
+
* Depending on which tokens are present, the authentication type is determined as follows:
|
|
12
|
+
* - authToken, guestToken =\> Guest authentication.
|
|
13
|
+
* - authToken, csrfToken, cookie =\> User authentication.
|
|
14
|
+
* - authToken, guestToken, cookie =\> Guest authentication while logging in.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export class AuthCredential implements IAuthCredential {
|
|
19
|
+
public authToken?: string;
|
|
20
|
+
public authenticationType?: AuthenticationType;
|
|
21
|
+
public cookies?: string;
|
|
22
|
+
public csrfToken?: string;
|
|
23
|
+
public guestToken?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param cookies - The list of cookies to be used for authenticating against Twitter.
|
|
27
|
+
* @param guestToken - The guest token to be used to authenticate a guest session.
|
|
28
|
+
*/
|
|
29
|
+
public constructor(cookies?: Cookie[], guestToken?: string) {
|
|
30
|
+
this.authToken =
|
|
31
|
+
'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
32
|
+
// If guest credentials given
|
|
33
|
+
if (!cookies && guestToken) {
|
|
34
|
+
this.guestToken = guestToken;
|
|
35
|
+
this.authenticationType = AuthenticationType.GUEST;
|
|
36
|
+
}
|
|
37
|
+
// If login credentials given
|
|
38
|
+
else if (cookies && guestToken) {
|
|
39
|
+
// Parsing the cookies
|
|
40
|
+
const parsedCookie: AuthCookie = new AuthCookie(cookies);
|
|
41
|
+
|
|
42
|
+
this.cookies = parsedCookie.toString();
|
|
43
|
+
this.guestToken = guestToken;
|
|
44
|
+
this.authenticationType = AuthenticationType.LOGIN;
|
|
45
|
+
}
|
|
46
|
+
// If user credentials given
|
|
47
|
+
else if (cookies && !guestToken) {
|
|
48
|
+
// Parsing the cookies
|
|
49
|
+
const parsedCookie: AuthCookie = new AuthCookie(cookies);
|
|
50
|
+
|
|
51
|
+
this.cookies = parsedCookie.toString();
|
|
52
|
+
this.csrfToken = parsedCookie.ct0;
|
|
53
|
+
this.authenticationType = AuthenticationType.USER;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns The HTTP header representation of 'this' object.
|
|
59
|
+
*/
|
|
60
|
+
public toHeader(): Record<string, string> {
|
|
61
|
+
const headers = {} as Record<string, string>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Conditionally initializing only those data which are supplied.
|
|
65
|
+
*
|
|
66
|
+
* This is done to ensure that the data that is not supplied, is not included in output, not even undefined.
|
|
67
|
+
*/
|
|
68
|
+
if (this.authToken) {
|
|
69
|
+
headers['authorization'] = `Bearer ${this.authToken}`;
|
|
70
|
+
}
|
|
71
|
+
if (this.guestToken) {
|
|
72
|
+
headers['x-guest-token'] = this.guestToken;
|
|
73
|
+
}
|
|
74
|
+
if (this.csrfToken) {
|
|
75
|
+
headers['x-csrf-token'] = this.csrfToken;
|
|
76
|
+
}
|
|
77
|
+
if (this.cookies) {
|
|
78
|
+
headers['cookie'] = this.cookies;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return headers;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { RawAnalyticsMetric } from '../../enums/raw/Analytics';
|
|
2
|
+
import { IAnalytics as IRawAnalytics } from '../../types/raw/base/Analytic';
|
|
3
|
+
|
|
4
|
+
import type { IAnalytics } from '../../types/data/Analytics';
|
|
5
|
+
import type { IAnalyticsMetric } from '../../types/raw/base/Analytic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The details of the analytic result of the connected User.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export class Analytics implements IAnalytics {
|
|
13
|
+
/** The raw analytic details. */
|
|
14
|
+
private readonly _raw: IRawAnalytics;
|
|
15
|
+
|
|
16
|
+
public bookmarks: number;
|
|
17
|
+
public createQuote: number;
|
|
18
|
+
public createReply: number;
|
|
19
|
+
public createTweets: number;
|
|
20
|
+
public createdAt: string;
|
|
21
|
+
public engagements: number;
|
|
22
|
+
public followers: number;
|
|
23
|
+
public follows: number;
|
|
24
|
+
public impressions: number;
|
|
25
|
+
public likes: number;
|
|
26
|
+
public organicMetricsTimeSeries: IAnalyticsMetric[];
|
|
27
|
+
public profileVisits: number;
|
|
28
|
+
public replies: number;
|
|
29
|
+
public retweets: number;
|
|
30
|
+
public shares: number;
|
|
31
|
+
public unfollows: number;
|
|
32
|
+
public verifiedFollowers: number;
|
|
33
|
+
|
|
34
|
+
public constructor(analytics: IRawAnalytics) {
|
|
35
|
+
this._raw = { ...analytics };
|
|
36
|
+
this.organicMetricsTimeSeries = analytics.organic_metrics_time_series;
|
|
37
|
+
this.createdAt = new Date().toISOString();
|
|
38
|
+
this.followers = analytics.relationship_counts.followers;
|
|
39
|
+
this.verifiedFollowers = parseInt(analytics.verified_follower_count, 10);
|
|
40
|
+
this.impressions = this._reduceMetrics(RawAnalyticsMetric.IMPRESSIONS);
|
|
41
|
+
this.profileVisits = this._reduceMetrics(RawAnalyticsMetric.PROFILE_VISITS);
|
|
42
|
+
this.engagements = this._reduceMetrics(RawAnalyticsMetric.ENGAGEMENTS);
|
|
43
|
+
this.follows = this._reduceMetrics(RawAnalyticsMetric.FOLLOWS);
|
|
44
|
+
this.replies = this._reduceMetrics(RawAnalyticsMetric.REPLIES);
|
|
45
|
+
this.likes = this._reduceMetrics(RawAnalyticsMetric.LIKES);
|
|
46
|
+
this.retweets = this._reduceMetrics(RawAnalyticsMetric.RETWEETS);
|
|
47
|
+
this.bookmarks = this._reduceMetrics(RawAnalyticsMetric.BOOKMARK);
|
|
48
|
+
this.shares = this._reduceMetrics(RawAnalyticsMetric.SHARE);
|
|
49
|
+
this.createTweets = this._reduceMetrics(RawAnalyticsMetric.CREATE_TWEET);
|
|
50
|
+
this.createQuote = this._reduceMetrics(RawAnalyticsMetric.CREATE_QUOTE);
|
|
51
|
+
this.createReply = this._reduceMetrics(RawAnalyticsMetric.CREATE_REPLY);
|
|
52
|
+
this.unfollows = this._reduceMetrics(RawAnalyticsMetric.UNFOLLOWS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The raw analytic details. */
|
|
56
|
+
public get raw(): IRawAnalytics {
|
|
57
|
+
return { ...this._raw };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reduces the organic metrics time series to a total value for a specific metric type.
|
|
62
|
+
*
|
|
63
|
+
* @param metricType - metricType The type of metric to reduce.
|
|
64
|
+
* @returns the total value of the specified metric type across all time series.
|
|
65
|
+
*/
|
|
66
|
+
private _reduceMetrics(metricType: RawAnalyticsMetric): number {
|
|
67
|
+
return this.organicMetricsTimeSeries.reduce((acc, metric) => {
|
|
68
|
+
const metricValue = metric.metric_values.find((m) => m.metric_type === (metricType as string));
|
|
69
|
+
return acc + (metricValue ? metricValue.metric_value : 0);
|
|
70
|
+
}, 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @returns A serializable JSON representation of `this` object.
|
|
75
|
+
*/
|
|
76
|
+
public toJSON(): IAnalytics {
|
|
77
|
+
return {
|
|
78
|
+
createdAt: this.createdAt,
|
|
79
|
+
followers: this.followers,
|
|
80
|
+
verifiedFollowers: this.verifiedFollowers,
|
|
81
|
+
impressions: this.impressions,
|
|
82
|
+
profileVisits: this.profileVisits,
|
|
83
|
+
engagements: this.engagements,
|
|
84
|
+
follows: this.follows,
|
|
85
|
+
replies: this.replies,
|
|
86
|
+
likes: this.likes,
|
|
87
|
+
retweets: this.retweets,
|
|
88
|
+
bookmarks: this.bookmarks,
|
|
89
|
+
shares: this.shares,
|
|
90
|
+
createTweets: this.createTweets,
|
|
91
|
+
createQuote: this.createQuote,
|
|
92
|
+
unfollows: this.unfollows,
|
|
93
|
+
createReply: this.createReply,
|
|
94
|
+
organicMetricsTimeSeries: this.organicMetricsTimeSeries,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LogActions } from '../../enums/Logging';
|
|
2
|
+
import { LogService } from '../../services/internal/LogService';
|
|
3
|
+
import { IBookmarkFolder } from '../../types/data/BookmarkFolder';
|
|
4
|
+
import { IBookmarkFolder as IRawBookmarkFolder } from '../../types/raw/base/BookmarkFolder';
|
|
5
|
+
import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The details of a single Bookmark Folder.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export class BookmarkFolder implements IBookmarkFolder {
|
|
13
|
+
/** The raw bookmark folder details. */
|
|
14
|
+
private readonly _raw: IRawBookmarkFolder;
|
|
15
|
+
|
|
16
|
+
public id: string;
|
|
17
|
+
public name: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param folder - The raw bookmark folder details.
|
|
21
|
+
*/
|
|
22
|
+
public constructor(folder: IRawBookmarkFolder) {
|
|
23
|
+
this._raw = { ...folder };
|
|
24
|
+
this.id = folder.id;
|
|
25
|
+
this.name = folder.name;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The raw bookmark folder details. */
|
|
29
|
+
public get raw(): IRawBookmarkFolder {
|
|
30
|
+
return { ...this._raw };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts and deserializes bookmark folders from the given raw response data.
|
|
35
|
+
*
|
|
36
|
+
* @param response - The raw response data.
|
|
37
|
+
*
|
|
38
|
+
* @returns The deserialized list of bookmark folders.
|
|
39
|
+
*/
|
|
40
|
+
public static list(response: NonNullable<unknown>): BookmarkFolder[] {
|
|
41
|
+
const folders: BookmarkFolder[] = [];
|
|
42
|
+
|
|
43
|
+
// Extract items from the response structure
|
|
44
|
+
const items = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result
|
|
45
|
+
?.bookmark_collections_slice?.items;
|
|
46
|
+
|
|
47
|
+
if (!items || !Array.isArray(items)) {
|
|
48
|
+
return folders;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Deserialize valid folders
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
if (item && item.id) {
|
|
54
|
+
// Logging
|
|
55
|
+
LogService.log(LogActions.DESERIALIZE, { id: item.id });
|
|
56
|
+
|
|
57
|
+
folders.push(new BookmarkFolder(item));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return folders;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @returns A serializable JSON representation of `this` object.
|
|
66
|
+
*/
|
|
67
|
+
public toJSON(): IBookmarkFolder {
|
|
68
|
+
return {
|
|
69
|
+
id: this.id,
|
|
70
|
+
name: this.name,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { IConversation } from '../../types/data/Conversation';
|
|
2
|
+
import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation';
|
|
3
|
+
import {
|
|
4
|
+
IInboxInitialResponse,
|
|
5
|
+
Conversation as RawConversation,
|
|
6
|
+
Conversations as RawConversations,
|
|
7
|
+
} from '../../types/raw/dm/InboxInitial';
|
|
8
|
+
import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline';
|
|
9
|
+
|
|
10
|
+
import { DirectMessage } from './DirectMessage';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Type guard to check if the response is an IConversationTimelineResponse
|
|
14
|
+
*/
|
|
15
|
+
function isConversationTimelineResponse(
|
|
16
|
+
response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse,
|
|
17
|
+
): response is IConversationTimelineResponse {
|
|
18
|
+
return 'conversation_timeline' in response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to check if the response is an IInboxInitialResponse
|
|
23
|
+
*/
|
|
24
|
+
function isInboxInitialResponse(
|
|
25
|
+
response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse,
|
|
26
|
+
): response is IInboxInitialResponse {
|
|
27
|
+
return 'inbox_initial_state' in response;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to check if the response is an IInboxTimelineResponse
|
|
32
|
+
*/
|
|
33
|
+
function isInboxTimelineResponse(
|
|
34
|
+
response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse,
|
|
35
|
+
): response is IInboxTimelineResponse {
|
|
36
|
+
return 'inbox_timeline' in response;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract typed conversation data from raw conversations object
|
|
41
|
+
*/
|
|
42
|
+
function extractConversationData(rawConversations: RawConversations): Array<[string, RawConversation]> {
|
|
43
|
+
return Object.entries(rawConversations);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The details of a single conversation.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export class Conversation implements IConversation {
|
|
52
|
+
/** The raw conversation details. */
|
|
53
|
+
private readonly _raw: RawConversation;
|
|
54
|
+
|
|
55
|
+
public avatarUrl?: string;
|
|
56
|
+
public hasMore: boolean;
|
|
57
|
+
public id: string;
|
|
58
|
+
public lastActivityAt: string;
|
|
59
|
+
public lastMessageId?: string;
|
|
60
|
+
public messages: DirectMessage[];
|
|
61
|
+
public muted: boolean;
|
|
62
|
+
public name?: string;
|
|
63
|
+
public notificationsDisabled: boolean;
|
|
64
|
+
public participants: string[];
|
|
65
|
+
public trusted: boolean;
|
|
66
|
+
public type: 'ONE_TO_ONE' | 'GROUP_DM';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param conversation - The raw conversation details from the API response.
|
|
70
|
+
* @param messages - Array of messages in this conversation.
|
|
71
|
+
*/
|
|
72
|
+
public constructor(conversation: unknown, messages: DirectMessage[] = []) {
|
|
73
|
+
this._raw = conversation as RawConversation;
|
|
74
|
+
|
|
75
|
+
const conv = conversation as Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
this.id = conv.conversation_id && typeof conv.conversation_id === 'string' ? conv.conversation_id : '';
|
|
78
|
+
this.type = this._parseConversationType(conv.type);
|
|
79
|
+
this.participants = this._parseParticipants(conv.participants);
|
|
80
|
+
this.name = conv.name && typeof conv.name === 'string' ? conv.name : undefined;
|
|
81
|
+
this.avatarUrl = this._parseAvatarUrl(conv);
|
|
82
|
+
this.trusted = Boolean(conv.trusted);
|
|
83
|
+
this.muted = Boolean(conv.muted);
|
|
84
|
+
this.notificationsDisabled = Boolean(conv.notifications_disabled);
|
|
85
|
+
this.lastActivityAt = this._parseTimestamp(conv.sort_timestamp);
|
|
86
|
+
this.lastMessageId =
|
|
87
|
+
conv.sort_event_id && typeof conv.sort_event_id === 'string' ? conv.sort_event_id : undefined;
|
|
88
|
+
this.hasMore = conv.status === 'HAS_MORE';
|
|
89
|
+
this.messages = messages;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The raw conversation details. */
|
|
93
|
+
public get raw(): RawConversation {
|
|
94
|
+
return this._raw;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse avatar URL from conversation data
|
|
99
|
+
*/
|
|
100
|
+
private _parseAvatarUrl(conv: Record<string, unknown>): string | undefined {
|
|
101
|
+
// Try avatar_image_https first
|
|
102
|
+
if (conv.avatar_image_https && typeof conv.avatar_image_https === 'string') {
|
|
103
|
+
return conv.avatar_image_https;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Try nested avatar.image.original_info.url
|
|
107
|
+
const avatar = conv.avatar as Record<string, unknown> | undefined;
|
|
108
|
+
const image = avatar?.image as Record<string, unknown> | undefined;
|
|
109
|
+
const originalInfo = image?.original_info as Record<string, unknown> | undefined;
|
|
110
|
+
|
|
111
|
+
if (originalInfo?.url && typeof originalInfo.url === 'string') {
|
|
112
|
+
return originalInfo.url;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse conversation type with proper fallback
|
|
120
|
+
*/
|
|
121
|
+
private _parseConversationType(type: unknown): 'ONE_TO_ONE' | 'GROUP_DM' {
|
|
122
|
+
if (type === 'ONE_TO_ONE' || type === 'GROUP_DM') {
|
|
123
|
+
return type;
|
|
124
|
+
}
|
|
125
|
+
return 'ONE_TO_ONE';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse participants array with type safety
|
|
130
|
+
*/
|
|
131
|
+
private _parseParticipants(participants: unknown): string[] {
|
|
132
|
+
if (!Array.isArray(participants)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return participants
|
|
137
|
+
.map((p) => {
|
|
138
|
+
if (p && typeof p === 'object' && 'user_id' in p) {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
140
|
+
const participantObj = p as { user_id: unknown };
|
|
141
|
+
if (typeof participantObj.user_id === 'string') {
|
|
142
|
+
return participantObj.user_id;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return '';
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse timestamp with proper fallback
|
|
152
|
+
*/
|
|
153
|
+
private _parseTimestamp(timestamp: unknown): string {
|
|
154
|
+
if (timestamp && (typeof timestamp === 'string' || typeof timestamp === 'number')) {
|
|
155
|
+
const date = new Date(Number(timestamp));
|
|
156
|
+
if (!isNaN(date.getTime())) {
|
|
157
|
+
return date.toISOString();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return new Date().toISOString();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extracts a single conversation from conversation timeline response.
|
|
165
|
+
*
|
|
166
|
+
* @param response - The raw response data.
|
|
167
|
+
*
|
|
168
|
+
* @returns The deserialized conversation with full message history.
|
|
169
|
+
*/
|
|
170
|
+
public static fromConversationTimeline(response: IConversationTimelineResponse): Conversation | undefined {
|
|
171
|
+
if (!response.conversation_timeline?.conversations) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rawConversations = response.conversation_timeline.conversations;
|
|
176
|
+
const entries = response.conversation_timeline.entries ?? [];
|
|
177
|
+
|
|
178
|
+
// Extract messages from entries
|
|
179
|
+
const messages: DirectMessage[] = [];
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if ('message' in entry && entry.message) {
|
|
182
|
+
messages.push(new DirectMessage(entry.message));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get the first (and typically only) conversation
|
|
187
|
+
const conversationEntries = extractConversationData(rawConversations);
|
|
188
|
+
const firstEntry = conversationEntries[0];
|
|
189
|
+
|
|
190
|
+
if (firstEntry) {
|
|
191
|
+
const [, conversationData] = firstEntry;
|
|
192
|
+
return new Conversation(conversationData, messages);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extracts conversations from inbox initial state response.
|
|
200
|
+
*
|
|
201
|
+
* @param response - The raw response data.
|
|
202
|
+
*
|
|
203
|
+
* @returns The deserialized list of conversations with their preview messages.
|
|
204
|
+
*/
|
|
205
|
+
public static listFromInboxInitial(response: IInboxInitialResponse): Conversation[] {
|
|
206
|
+
const conversations: Conversation[] = [];
|
|
207
|
+
|
|
208
|
+
if (!response.inbox_initial_state?.conversations) {
|
|
209
|
+
return conversations;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const rawConversations = response.inbox_initial_state.conversations;
|
|
213
|
+
const entries = response.inbox_initial_state.entries ?? [];
|
|
214
|
+
|
|
215
|
+
// Group messages by conversation ID
|
|
216
|
+
const messagesByConversation = new Map<string, DirectMessage[]>();
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if ('message' in entry && entry.message) {
|
|
219
|
+
const message = new DirectMessage(entry.message);
|
|
220
|
+
const convId = message.conversationId;
|
|
221
|
+
if (convId) {
|
|
222
|
+
if (!messagesByConversation.has(convId)) {
|
|
223
|
+
messagesByConversation.set(convId, []);
|
|
224
|
+
}
|
|
225
|
+
messagesByConversation.get(convId)!.push(message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create conversations with their messages
|
|
231
|
+
const conversationEntries = extractConversationData(rawConversations);
|
|
232
|
+
for (const [, conversation] of conversationEntries) {
|
|
233
|
+
const convId = (conversation as unknown as Record<string, unknown>).conversation_id as string;
|
|
234
|
+
const messages = messagesByConversation.get(convId) ?? [];
|
|
235
|
+
conversations.push(new Conversation(conversation, messages));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return conversations;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extracts conversations from inbox timeline response.
|
|
243
|
+
*
|
|
244
|
+
* @param response - The raw response data.
|
|
245
|
+
*
|
|
246
|
+
* @returns The deserialized list of conversations with their messages.
|
|
247
|
+
*/
|
|
248
|
+
public static listFromInboxTimeline(response: IInboxTimelineResponse): Conversation[] {
|
|
249
|
+
const conversations: Conversation[] = [];
|
|
250
|
+
|
|
251
|
+
if (!response.inbox_timeline?.conversations) {
|
|
252
|
+
return conversations;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const rawConversations = response.inbox_timeline.conversations;
|
|
256
|
+
const entries = response.inbox_timeline.entries ?? [];
|
|
257
|
+
|
|
258
|
+
// Group messages by conversation ID
|
|
259
|
+
const messagesByConversation = new Map<string, DirectMessage[]>();
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
if ('message' in entry && entry.message) {
|
|
262
|
+
const message = new DirectMessage(entry.message);
|
|
263
|
+
const convId = message.conversationId;
|
|
264
|
+
if (convId) {
|
|
265
|
+
if (!messagesByConversation.has(convId)) {
|
|
266
|
+
messagesByConversation.set(convId, []);
|
|
267
|
+
}
|
|
268
|
+
messagesByConversation.get(convId)!.push(message);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Create conversations with their messages
|
|
274
|
+
const conversationEntries = extractConversationData(rawConversations);
|
|
275
|
+
for (const [, conversation] of conversationEntries) {
|
|
276
|
+
const convId = (conversation as unknown as Record<string, unknown>).conversation_id as string;
|
|
277
|
+
const messages = messagesByConversation.get(convId) ?? [];
|
|
278
|
+
conversations.push(new Conversation(conversation, messages));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return conversations;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generic method to extract conversations from any supported response type
|
|
286
|
+
*/
|
|
287
|
+
public static listFromResponse(
|
|
288
|
+
response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse,
|
|
289
|
+
): Conversation[] {
|
|
290
|
+
if (isConversationTimelineResponse(response)) {
|
|
291
|
+
const conversation = Conversation.fromConversationTimeline(response);
|
|
292
|
+
return conversation ? [conversation] : [];
|
|
293
|
+
} else if (isInboxInitialResponse(response)) {
|
|
294
|
+
return Conversation.listFromInboxInitial(response);
|
|
295
|
+
} else if (isInboxTimelineResponse(response)) {
|
|
296
|
+
return Conversation.listFromInboxTimeline(response);
|
|
297
|
+
}
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get the other participant's ID (only for one-to-one conversations)
|
|
303
|
+
*/
|
|
304
|
+
public getOtherParticipant(currentUserId: string): string | undefined {
|
|
305
|
+
if (!this.isOneToOne() || this.participants.length !== 2) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
return this.participants.find((id) => id !== currentUserId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if this conversation is a group DM
|
|
313
|
+
*/
|
|
314
|
+
public isGroupDM(): boolean {
|
|
315
|
+
return this.type === 'GROUP_DM';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if this conversation is one-to-one
|
|
320
|
+
*/
|
|
321
|
+
public isOneToOne(): boolean {
|
|
322
|
+
return this.type === 'ONE_TO_ONE';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @returns A serializable JSON representation of `this` object.
|
|
327
|
+
*/
|
|
328
|
+
public toJSON(): IConversation {
|
|
329
|
+
return {
|
|
330
|
+
avatarUrl: this.avatarUrl,
|
|
331
|
+
hasMore: this.hasMore,
|
|
332
|
+
id: this.id,
|
|
333
|
+
lastActivityAt: this.lastActivityAt,
|
|
334
|
+
lastMessageId: this.lastMessageId,
|
|
335
|
+
messages: this.messages.map((msg) => msg.toJSON()),
|
|
336
|
+
muted: this.muted,
|
|
337
|
+
name: this.name,
|
|
338
|
+
notificationsDisabled: this.notificationsDisabled,
|
|
339
|
+
participants: this.participants,
|
|
340
|
+
trusted: this.trusted,
|
|
341
|
+
type: this.type,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BaseType } from '../../enums/Data';
|
|
2
|
+
|
|
3
|
+
import { findByFilter } from '../../helper/JsonUtils';
|
|
4
|
+
|
|
5
|
+
import { ICursoredData } from '../../types/data/CursoredData';
|
|
6
|
+
import { ICursor as IRawCursor } from '../../types/raw/base/Cursor';
|
|
7
|
+
import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders';
|
|
8
|
+
|
|
9
|
+
import { BookmarkFolder } from './BookmarkFolder';
|
|
10
|
+
import { List } from './List';
|
|
11
|
+
import { Notification } from './Notification';
|
|
12
|
+
import { Tweet } from './Tweet';
|
|
13
|
+
import { User } from './User';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The data that is fetched batch-wise using a cursor.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam T - Type of data to be stored.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export class CursoredData<T extends Notification | Tweet | User | List | BookmarkFolder> implements ICursoredData<T> {
|
|
23
|
+
public list: T[];
|
|
24
|
+
public next: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param response - The raw response.
|
|
28
|
+
* @param type - The base type of the data included in the batch.
|
|
29
|
+
*/
|
|
30
|
+
public constructor(response: NonNullable<unknown>, type: BaseType) {
|
|
31
|
+
// Initializing defaults
|
|
32
|
+
this.list = [];
|
|
33
|
+
this.next = '';
|
|
34
|
+
|
|
35
|
+
if (type == BaseType.TWEET) {
|
|
36
|
+
this.list = Tweet.timeline(response) as T[];
|
|
37
|
+
this.next = findByFilter<IRawCursor>(response, 'cursorType', 'Bottom')[0]?.value ?? '';
|
|
38
|
+
} else if (type == BaseType.USER) {
|
|
39
|
+
this.list = User.timeline(response) as T[];
|
|
40
|
+
this.next = findByFilter<IRawCursor>(response, 'cursorType', 'Bottom')[0]?.value ?? '';
|
|
41
|
+
} else if (type == BaseType.LIST) {
|
|
42
|
+
this.list = List.timeline(response) as T[];
|
|
43
|
+
this.next = findByFilter<IRawCursor>(response, 'cursorType', 'Bottom')[0]?.value ?? '';
|
|
44
|
+
} else if (type == BaseType.NOTIFICATION) {
|
|
45
|
+
this.list = Notification.list(response) as T[];
|
|
46
|
+
this.next = findByFilter<IRawCursor>(response, 'cursorType', 'Bottom')[0]?.value ?? '';
|
|
47
|
+
} else if (type == BaseType.BOOKMARK_FOLDER) {
|
|
48
|
+
this.list = BookmarkFolder.list(response) as T[];
|
|
49
|
+
const sliceInfo = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result
|
|
50
|
+
?.bookmark_collections_slice?.slice_info;
|
|
51
|
+
this.next = sliceInfo?.next_cursor ?? '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @returns A serializable JSON representation of `this` object.
|
|
57
|
+
*/
|
|
58
|
+
public toJSON(): ICursoredData<T> {
|
|
59
|
+
return {
|
|
60
|
+
list: this.list.map((item) => item.toJSON() as T),
|
|
61
|
+
next: this.next,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|