rettiwt-api 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +2 -0
- package/.gitattributes +3 -0
- package/Dockerfile +9 -0
- package/README.md +38 -0
- package/dist/config/env.d.ts +5 -0
- package/dist/config/env.js +9 -0
- package/dist/config/env.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/models/graphql/Global.d.ts +4 -0
- package/dist/models/graphql/Global.js +13 -0
- package/dist/models/graphql/Global.js.map +1 -0
- package/dist/models/graphql/TweetTypes.d.ts +6 -0
- package/dist/models/graphql/TweetTypes.js +156 -0
- package/dist/models/graphql/TweetTypes.js.map +1 -0
- package/dist/models/graphql/UserTypes.d.ts +3 -0
- package/dist/models/graphql/UserTypes.js +139 -0
- package/dist/models/graphql/UserTypes.js.map +1 -0
- package/dist/queries/RootQuery.d.ts +4 -0
- package/dist/queries/RootQuery.js +59 -0
- package/dist/queries/RootQuery.js.map +1 -0
- package/dist/resolvers/ResolverBase.d.ts +5 -0
- package/dist/resolvers/ResolverBase.js +11 -0
- package/dist/resolvers/ResolverBase.js.map +1 -0
- package/dist/resolvers/TweetResolver.d.ts +54 -0
- package/dist/resolvers/TweetResolver.js +328 -0
- package/dist/resolvers/TweetResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts +47 -0
- package/dist/resolvers/UserResolver.js +302 -0
- package/dist/resolvers/UserResolver.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +75 -0
- package/dist/server.js.map +1 -0
- package/dist/services/AuthService.d.ts +17 -0
- package/dist/services/AuthService.js +103 -0
- package/dist/services/AuthService.js.map +1 -0
- package/dist/services/CacheService.d.ts +25 -0
- package/dist/services/CacheService.js +141 -0
- package/dist/services/CacheService.js.map +1 -0
- package/dist/services/FetcherService.d.ts +30 -0
- package/dist/services/FetcherService.js +171 -0
- package/dist/services/FetcherService.js.map +1 -0
- package/dist/services/data/TweetService.d.ts +43 -0
- package/dist/services/data/TweetService.js +229 -0
- package/dist/services/data/TweetService.js.map +1 -0
- package/dist/services/data/UserAccountService.d.ts +49 -0
- package/dist/services/data/UserAccountService.js +250 -0
- package/dist/services/data/UserAccountService.js.map +1 -0
- package/dist/services/helper/Deserializers.d.ts +19 -0
- package/dist/services/helper/Deserializers.js +115 -0
- package/dist/services/helper/Deserializers.js.map +1 -0
- package/dist/services/helper/Extractors.d.ts +101 -0
- package/dist/services/helper/Extractors.js +409 -0
- package/dist/services/helper/Extractors.js.map +1 -0
- package/dist/services/helper/Headers.d.ts +9 -0
- package/dist/services/helper/Headers.js +47 -0
- package/dist/services/helper/Headers.js.map +1 -0
- package/dist/services/helper/Parser.d.ts +28 -0
- package/dist/services/helper/Parser.js +104 -0
- package/dist/services/helper/Parser.js.map +1 -0
- package/dist/services/helper/Urls.d.ts +74 -0
- package/dist/services/helper/Urls.js +114 -0
- package/dist/services/helper/Urls.js.map +1 -0
- package/dist/test/Test.js +2 -0
- package/dist/test/Test.js.map +1 -0
- package/dist/types/Authentication.d.ts +15 -0
- package/dist/types/Authentication.js +5 -0
- package/dist/types/Authentication.js.map +1 -0
- package/dist/types/HTTP.d.ts +22 -0
- package/dist/types/HTTP.js +30 -0
- package/dist/types/HTTP.js.map +1 -0
- package/dist/types/Service.d.ts +27 -0
- package/dist/types/Service.js +19 -0
- package/dist/types/Service.js.map +1 -0
- package/dist/types/Tweet.d.ts +40 -0
- package/dist/types/Tweet.js +5 -0
- package/dist/types/Tweet.js.map +1 -0
- package/dist/types/UserAccount.d.ts +19 -0
- package/dist/types/UserAccount.js +4 -0
- package/dist/types/UserAccount.js.map +1 -0
- package/dist/types/graphql/Errors.d.ts +15 -0
- package/dist/types/graphql/Errors.js +23 -0
- package/dist/types/graphql/Errors.js.map +1 -0
- package/dist/types/raw/auth/Cookie.d.ts +16 -0
- package/dist/types/raw/auth/Cookie.js +3 -0
- package/dist/types/raw/auth/Cookie.js.map +1 -0
- package/dist/types/raw/tweet/Favouriters.d.ts +164 -0
- package/dist/types/raw/tweet/Favouriters.js +3 -0
- package/dist/types/raw/tweet/Favouriters.js.map +1 -0
- package/dist/types/raw/tweet/Retweeters.d.ts +171 -0
- package/dist/types/raw/tweet/Retweeters.js +3 -0
- package/dist/types/raw/tweet/Retweeters.js.map +1 -0
- package/dist/types/raw/tweet/Tweet.d.ts +746 -0
- package/dist/types/raw/tweet/Tweet.js +3 -0
- package/dist/types/raw/tweet/Tweet.js.map +1 -0
- package/dist/types/raw/tweet/Tweets.d.ts +386 -0
- package/dist/types/raw/tweet/Tweets.js +3 -0
- package/dist/types/raw/tweet/Tweets.js.map +1 -0
- package/dist/types/raw/user/Followers.d.ts +176 -0
- package/dist/types/raw/user/Followers.js +3 -0
- package/dist/types/raw/user/Followers.js.map +1 -0
- package/dist/types/raw/user/Following.d.ts +176 -0
- package/dist/types/raw/user/Following.js +3 -0
- package/dist/types/raw/user/Following.js.map +1 -0
- package/dist/types/raw/user/Likes.d.ts +1059 -0
- package/dist/types/raw/user/Likes.js +3 -0
- package/dist/types/raw/user/Likes.js.map +1 -0
- package/dist/types/raw/user/Tweets.d.ts +2428 -0
- package/dist/types/raw/user/Tweets.js +3 -0
- package/dist/types/raw/user/Tweets.js.map +1 -0
- package/dist/types/raw/user/User.d.ts +117 -0
- package/dist/types/raw/user/User.js +3 -0
- package/dist/types/raw/user/User.js.map +1 -0
- package/dist/types/raw/user/Users.d.ts +120 -0
- package/dist/types/raw/user/Users.js +3 -0
- package/dist/types/raw/user/Users.js.map +1 -0
- package/environment.d.ts +11 -0
- package/package.json +40 -0
- package/src/config/env.ts +5 -0
- package/src/index.ts +19 -0
- package/src/models/graphql/Global.ts +10 -0
- package/src/models/graphql/TweetTypes.ts +154 -0
- package/src/models/graphql/UserTypes.ts +137 -0
- package/src/queries/RootQuery.ts +56 -0
- package/src/resolvers/ResolverBase.ts +12 -0
- package/src/resolvers/TweetResolver.ts +257 -0
- package/src/resolvers/UserResolver.ts +239 -0
- package/src/server.ts +36 -0
- package/src/services/AuthService.ts +58 -0
- package/src/services/CacheService.ts +70 -0
- package/src/services/FetcherService.ts +84 -0
- package/src/services/data/TweetService.ts +163 -0
- package/src/services/data/UserAccountService.ts +187 -0
- package/src/services/helper/Deserializers.ts +95 -0
- package/src/services/helper/Extractors.ts +455 -0
- package/src/services/helper/Headers.ts +45 -0
- package/src/services/helper/Parser.ts +108 -0
- package/src/services/helper/Urls.ts +109 -0
- package/src/types/Authentication.ts +16 -0
- package/src/types/HTTP.ts +23 -0
- package/src/types/Service.ts +36 -0
- package/src/types/Tweet.ts +44 -0
- package/src/types/UserAccount.ts +21 -0
- package/src/types/graphql/Errors.ts +16 -0
- package/src/types/raw/auth/Cookie.ts +16 -0
- package/src/types/raw/tweet/Favouriters.ts +193 -0
- package/src/types/raw/tweet/Retweeters.ts +201 -0
- package/src/types/raw/tweet/Tweet.ts +882 -0
- package/src/types/raw/tweet/Tweets.ts +444 -0
- package/src/types/raw/user/Followers.ts +208 -0
- package/src/types/raw/user/Following.ts +208 -0
- package/src/types/raw/user/Likes.ts +1247 -0
- package/src/types/raw/user/Tweets.ts +2847 -0
- package/src/types/raw/user/User.ts +135 -0
- package/tsconfig.json +95 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// TYPES
|
|
2
|
+
import { DataErrors } from '../../types/graphql/Errors';
|
|
3
|
+
import RawUser from '../../types/raw/user/User';
|
|
4
|
+
import RawUserFollowers from '../../types/raw/user/Followers';
|
|
5
|
+
import RawUserFollowing from '../../types/raw/user/Following';
|
|
6
|
+
import RawUserLikes from '../../types/raw/user/Likes';
|
|
7
|
+
import RawUserTweets from '../../types/raw/user/Tweets';
|
|
8
|
+
import RawTweet from '../../types/raw/tweet/Tweet';
|
|
9
|
+
import RawTweets from '../../types/raw/tweet/Tweets';
|
|
10
|
+
import RawLikers from '../../types/raw/tweet/Favouriters';
|
|
11
|
+
import RawRetweeters from '../../types/raw/tweet/Retweeters';
|
|
12
|
+
|
|
13
|
+
// PARSERS
|
|
14
|
+
import * as Parsers from './Parser';
|
|
15
|
+
|
|
16
|
+
/* USERS */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns The raw user account data formatted and sorted into required and additional data
|
|
20
|
+
* @param res The raw response received from Twitter
|
|
21
|
+
*/
|
|
22
|
+
export function extractUserAccountDetails(res: RawUser): {
|
|
23
|
+
required: any[],
|
|
24
|
+
cursor: string,
|
|
25
|
+
users: any[],
|
|
26
|
+
tweets: any[]
|
|
27
|
+
} {
|
|
28
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
29
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
30
|
+
let users: any[] = []; // To store additional user data
|
|
31
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
32
|
+
|
|
33
|
+
// If user not found or account suspended
|
|
34
|
+
if (Parsers.isJSONEmpty(res.data) || Parsers.isJSONEmpty(res.data.user) || res.data.user.result.__typename !== 'User') {
|
|
35
|
+
throw new Error(DataErrors.UserNotFound);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Destructuring user account data
|
|
39
|
+
required.push(res.data.user.result);
|
|
40
|
+
users.push(res.data.user.result);
|
|
41
|
+
|
|
42
|
+
// Returning the data
|
|
43
|
+
return {
|
|
44
|
+
required: required,
|
|
45
|
+
cursor: cursor,
|
|
46
|
+
users: users,
|
|
47
|
+
tweets: tweets
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns The raw user following/followers data formatted and sorted into required and additional data
|
|
53
|
+
* @param res The raw response received from TwitterAPI
|
|
54
|
+
*/
|
|
55
|
+
export function extractUserFollow(res: RawUserFollowers | RawUserFollowing): {
|
|
56
|
+
required: any[],
|
|
57
|
+
cursor: string,
|
|
58
|
+
users: any[],
|
|
59
|
+
tweets: any[]
|
|
60
|
+
} {
|
|
61
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
62
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
63
|
+
let users: any[] = []; // To store additional user data
|
|
64
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
65
|
+
|
|
66
|
+
// If user does not exist
|
|
67
|
+
if (Parsers.isJSONEmpty(res.data.user)) {
|
|
68
|
+
throw new Error(DataErrors.UserNotFound);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extracting the raw list
|
|
72
|
+
res.data.user.result.timeline.timeline.instructions.forEach(item => {
|
|
73
|
+
if (item.type === 'TimelineAddEntries') {
|
|
74
|
+
// Destructuring data
|
|
75
|
+
item.entries?.forEach(entry => {
|
|
76
|
+
// If entry is of type user and user account exists
|
|
77
|
+
if (entry.entryId.indexOf('user') != -1 && entry.content.itemContent?.user_results.result.__typename ==='User') {
|
|
78
|
+
required.push(entry.content.itemContent.user_results.result);
|
|
79
|
+
users.push(entry.content.itemContent.user_results.result);
|
|
80
|
+
}
|
|
81
|
+
// If entry is of type cursor
|
|
82
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
83
|
+
cursor = entry.content.value ?? '';
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Returning the data
|
|
90
|
+
return {
|
|
91
|
+
required: required,
|
|
92
|
+
cursor: cursor,
|
|
93
|
+
users: users,
|
|
94
|
+
tweets: tweets
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @returns The raw user likes data formatted and sorted into required and additional data
|
|
100
|
+
* @param res The raw response received from TwitterAPI
|
|
101
|
+
*/
|
|
102
|
+
export function extractUserLikes(res: RawUserLikes): {
|
|
103
|
+
required: any[],
|
|
104
|
+
cursor: string,
|
|
105
|
+
users: any[],
|
|
106
|
+
tweets: any[]
|
|
107
|
+
} {
|
|
108
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
109
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
110
|
+
let users: any[] = []; // To store additional user data
|
|
111
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
112
|
+
|
|
113
|
+
// If user does not exist
|
|
114
|
+
if (Parsers.isJSONEmpty(res.data.user)) {
|
|
115
|
+
throw new Error(DataErrors.UserNotFound);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extracting the raw list
|
|
119
|
+
res.data.user.result.timeline_v2.timeline.instructions.forEach(item => {
|
|
120
|
+
if (item.type === 'TimelineAddEntries') {
|
|
121
|
+
// Destructuring data
|
|
122
|
+
item.entries.forEach(entry => {
|
|
123
|
+
// If entry is of type tweet and tweet exists
|
|
124
|
+
if (entry.entryId.indexOf('tweet') != -1 && entry.content.itemContent?.tweet_results.result.__typename === 'Tweet') {
|
|
125
|
+
required.push(entry.content.itemContent.tweet_results.result);
|
|
126
|
+
users.push(entry.content.itemContent.tweet_results.result.core.user_results.result);
|
|
127
|
+
tweets.push(entry.content.itemContent.tweet_results.result);
|
|
128
|
+
}
|
|
129
|
+
// If entry is of type cursor
|
|
130
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
131
|
+
cursor = entry.content.value ?? '';
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Returning the data
|
|
138
|
+
return {
|
|
139
|
+
required: required,
|
|
140
|
+
cursor: cursor,
|
|
141
|
+
users: users,
|
|
142
|
+
tweets: tweets
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @returns The raw tweets data formatted and sorted into required and additional data
|
|
148
|
+
* @param res The raw response received from TwitterAPI
|
|
149
|
+
*/
|
|
150
|
+
export function extractUserTweets(res: RawUserTweets): {
|
|
151
|
+
required: any[],
|
|
152
|
+
cursor: string,
|
|
153
|
+
users: any[],
|
|
154
|
+
tweets: any[]
|
|
155
|
+
} {
|
|
156
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
157
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
158
|
+
let users: any[] = []; // To store additional user data
|
|
159
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
160
|
+
|
|
161
|
+
// Getting the raw tweet list
|
|
162
|
+
let dataTweets = res.data.user.result.timeline_v2.timeline.instructions.filter(item => item.type === 'TimelineAddEntries')[0].entries;
|
|
163
|
+
|
|
164
|
+
// Destructuring tweets, if not empty
|
|
165
|
+
if (!Parsers.isJSONEmpty(dataTweets)) {
|
|
166
|
+
// Iterating through the json array of tweets
|
|
167
|
+
for (let entry of dataTweets) {
|
|
168
|
+
// If the entry is a tweet
|
|
169
|
+
if(entry.entryId.indexOf('tweet') != -1) {
|
|
170
|
+
required.push(entry.content.itemContent?.tweet_results.result);
|
|
171
|
+
tweets.push(entry.content.itemContent?.tweet_results.result);
|
|
172
|
+
users.push(entry.content.itemContent?.tweet_results.result.core.user_results.result);
|
|
173
|
+
}
|
|
174
|
+
// If the entry is a cursor
|
|
175
|
+
else if(entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
176
|
+
cursor = entry.content.value as string;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
required: required,
|
|
183
|
+
cursor: cursor,
|
|
184
|
+
users: users,
|
|
185
|
+
tweets: tweets
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* TWEETS */
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @returns The raw tweets data formatted and sorted into required and additional data
|
|
193
|
+
* @param res The raw response received from TwitterAPI
|
|
194
|
+
*/
|
|
195
|
+
export function extractTweets(res: RawTweets): {
|
|
196
|
+
required: any[],
|
|
197
|
+
cursor: string,
|
|
198
|
+
users: any[],
|
|
199
|
+
tweets: any[]
|
|
200
|
+
} {
|
|
201
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
202
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
203
|
+
let users: any[] = []; // To store additional user data
|
|
204
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
205
|
+
|
|
206
|
+
// Getting raw tweet list
|
|
207
|
+
let dataTweets = res.globalObjects.tweets;
|
|
208
|
+
|
|
209
|
+
// Getting raw users list
|
|
210
|
+
let dataUsers = res.globalObjects.users;
|
|
211
|
+
|
|
212
|
+
// Destructuring tweets, if not empty
|
|
213
|
+
if (!Parsers.isJSONEmpty(dataTweets)) {
|
|
214
|
+
// Iterating through the json array of tweets
|
|
215
|
+
for (let key of Object.keys(dataTweets)) {
|
|
216
|
+
required.push({ rest_id: dataTweets[key].id_str, legacy: dataTweets[key] });
|
|
217
|
+
tweets.push({ rest_id: dataTweets[key].id_str, legacy: dataTweets[key] });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Destructuring users, if not empty
|
|
222
|
+
if (!Parsers.isJSONEmpty(dataUsers)) {
|
|
223
|
+
// Iterating through the json array of users
|
|
224
|
+
for (let key of Object.keys(dataUsers)) {
|
|
225
|
+
users.push({ rest_id: dataUsers[key].id_str, legacy: dataUsers[key] });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Getting the cursor to next batch
|
|
230
|
+
// If not first batch
|
|
231
|
+
if (res.timeline.instructions.length > 2) {
|
|
232
|
+
cursor = res.timeline.instructions[2]?.replaceEntry.entry.content.operation?.cursor.value ?? '';
|
|
233
|
+
}
|
|
234
|
+
// If first batch
|
|
235
|
+
else {
|
|
236
|
+
cursor = res.timeline.instructions[0].addEntries?.entries.filter(item => item.entryId.indexOf('cursor-bottom') != -1)[0].content.operation?.cursor.value ?? '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Returning the data
|
|
240
|
+
return {
|
|
241
|
+
required: required,
|
|
242
|
+
cursor: cursor,
|
|
243
|
+
users: users,
|
|
244
|
+
tweets: tweets
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @returns The raw tweet data formatted and sorted into required and additional data
|
|
250
|
+
* @param res The raw response received from TwitterAPI
|
|
251
|
+
* @param tweetId The rest id of the tweet to fetch
|
|
252
|
+
*/
|
|
253
|
+
export function extractTweet(res: RawTweet, tweetId: string): {
|
|
254
|
+
required: any[],
|
|
255
|
+
cursor: string,
|
|
256
|
+
users: any[],
|
|
257
|
+
tweets: any[]
|
|
258
|
+
} {
|
|
259
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
260
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
261
|
+
let users: any[] = []; // To store additional user data
|
|
262
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
263
|
+
|
|
264
|
+
// If tweet does not exist
|
|
265
|
+
if (Parsers.isJSONEmpty(res.data)) {
|
|
266
|
+
throw new Error(DataErrors.TweetNotFound);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Destructuring the received raw data
|
|
270
|
+
res.data.threaded_conversation_with_injections.instructions.filter(item => item['type'] === 'TimelineAddEntries')[0].entries?.forEach(entry => {
|
|
271
|
+
// If entry is of type tweet and tweet exists
|
|
272
|
+
if (entry.entryId.indexOf('tweet') != -1 && entry.content.itemContent?.tweet_results?.result.__typename === 'Tweet') {
|
|
273
|
+
// If this is the required tweet
|
|
274
|
+
if (entry.entryId.indexOf(tweetId) != -1) {
|
|
275
|
+
required.push(entry.content.itemContent.tweet_results.result);
|
|
276
|
+
}
|
|
277
|
+
tweets.push(entry.content.itemContent.tweet_results.result);
|
|
278
|
+
users.push(entry.content.itemContent.tweet_results.result.core.user_results.result);
|
|
279
|
+
}
|
|
280
|
+
// If entry if of type conversation
|
|
281
|
+
else if (entry.entryId.indexOf('conversationthread') != -1) {
|
|
282
|
+
// Iterating over the conversation
|
|
283
|
+
entry.content.items?.forEach(item => {
|
|
284
|
+
// If item is of type tweet and tweet exists
|
|
285
|
+
if (item.entryId.indexOf('tweet') != -1 && item.item.itemContent.tweet_results?.result.__typename === 'Tweet') {
|
|
286
|
+
required.push(item.item.itemContent.tweet_results.result);
|
|
287
|
+
tweets.push(item.item.itemContent.tweet_results.result);
|
|
288
|
+
users.push(item.item.itemContent.tweet_results.result.core.user_results.result);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Returning the data
|
|
295
|
+
return {
|
|
296
|
+
required: required,
|
|
297
|
+
cursor: cursor,
|
|
298
|
+
users: users,
|
|
299
|
+
tweets: tweets
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @returns The raw tweet likers data formatted and sorted into required and additional data
|
|
305
|
+
* @param res The raw response received from TwitterAPI
|
|
306
|
+
*/
|
|
307
|
+
export function extractTweetLikers(res: RawLikers): {
|
|
308
|
+
required: any[],
|
|
309
|
+
cursor: string,
|
|
310
|
+
users: any[],
|
|
311
|
+
tweets: any[]
|
|
312
|
+
} {
|
|
313
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
314
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
315
|
+
let users: any[] = []; // To store additional user data
|
|
316
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
317
|
+
|
|
318
|
+
// If tweet does not exist
|
|
319
|
+
if (Parsers.isJSONEmpty(res.data.favoriters_timeline)) {
|
|
320
|
+
throw new Error(DataErrors.TweetNotFound);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Destructuring raw list of likers
|
|
324
|
+
res.data.favoriters_timeline.timeline.instructions.filter(item => item.type === 'TimelineAddEntries')[0].entries.forEach(entry => {
|
|
325
|
+
// If entry is of type user and user exists
|
|
326
|
+
if (entry.entryId.indexOf('user') != -1 && entry.content.itemContent?.user_results.result.__typename === 'User') {
|
|
327
|
+
required.push(entry.content.itemContent.user_results.result);
|
|
328
|
+
users.push(entry.content.itemContent.user_results.result);
|
|
329
|
+
}
|
|
330
|
+
// If entry is of type cursor
|
|
331
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
332
|
+
cursor = entry.content.value ?? '';
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Returning the data
|
|
337
|
+
return {
|
|
338
|
+
required: required,
|
|
339
|
+
cursor: cursor,
|
|
340
|
+
users: users,
|
|
341
|
+
tweets: tweets
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @returns The raw tweet retweeters data formatted and sorted into required and additional data
|
|
347
|
+
* @param res The raw response received from TwitterAPI
|
|
348
|
+
*/
|
|
349
|
+
export function extractTweetRetweeters(res: RawRetweeters): {
|
|
350
|
+
required: any[],
|
|
351
|
+
cursor: string,
|
|
352
|
+
users: any[],
|
|
353
|
+
tweets: any[]
|
|
354
|
+
} {
|
|
355
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
356
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
357
|
+
let users: any[] = []; // To store additional user data
|
|
358
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
359
|
+
|
|
360
|
+
// If tweet does not exist
|
|
361
|
+
if (Parsers.isJSONEmpty(res.data.retweeters_timeline)) {
|
|
362
|
+
throw new Error(DataErrors.TweetNotFound);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Destructuring raw list of retweeters
|
|
366
|
+
res.data.retweeters_timeline.timeline.instructions.filter(item => item.type === 'TimelineAddEntries')[0].entries.forEach(entry => {
|
|
367
|
+
// If entry is of type user and user exists
|
|
368
|
+
if (entry.entryId.indexOf('user') != -1 && entry.content.itemContent?.user_results.result.__typename === 'User') {
|
|
369
|
+
required.push(entry.content.itemContent.user_results.result);
|
|
370
|
+
users.push(entry.content.itemContent.user_results.result);
|
|
371
|
+
}
|
|
372
|
+
// If entry is of type cursor
|
|
373
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
374
|
+
cursor = entry.content.value ?? '';
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Returning the data
|
|
379
|
+
return {
|
|
380
|
+
required: required,
|
|
381
|
+
cursor: cursor,
|
|
382
|
+
users: users,
|
|
383
|
+
tweets: tweets
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @returns The raw tweet replies data formatted and sorted into required and additional data
|
|
389
|
+
* @param res The raw response received from TwitterAPI
|
|
390
|
+
* @param tweetId The id of the tweet whose replies must be extracted
|
|
391
|
+
*/
|
|
392
|
+
export function extractTweetReplies(res: RawTweet, tweetId: string): {
|
|
393
|
+
required: any[],
|
|
394
|
+
cursor: string,
|
|
395
|
+
users: any[],
|
|
396
|
+
tweets: any[]
|
|
397
|
+
} {
|
|
398
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
399
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
400
|
+
let users: any[] = []; // To store additional user data
|
|
401
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
402
|
+
|
|
403
|
+
// If tweet does not exist
|
|
404
|
+
if (Parsers.isJSONEmpty(res.data)) {
|
|
405
|
+
throw new Error(DataErrors.TweetNotFound);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Destructuring the received raw data
|
|
409
|
+
//@ts-ignore
|
|
410
|
+
res.data.threaded_conversation_with_injections.instructions.filter(item => item.type === 'TimelineAddEntries')[0].entries.map(entry => {
|
|
411
|
+
// If entry is of type tweet
|
|
412
|
+
if (entry.entryId.indexOf('tweet') != -1) {
|
|
413
|
+
// If tweet exists
|
|
414
|
+
if(entry.content.itemContent?.tweet_results?.result.__typename === 'Tweet') {
|
|
415
|
+
tweets.push(entry.content.itemContent.tweet_results.result);
|
|
416
|
+
users.push(entry.content.itemContent.tweet_results.result.core.user_results.result);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// If entry if of type conversation/reply
|
|
420
|
+
else if (entry.entryId.indexOf('conversationthread') != -1) {
|
|
421
|
+
// If tweet exists
|
|
422
|
+
if(entry.content.items?.at(0)?.item.itemContent.tweet_results?.result.__typename === 'Tweet') {
|
|
423
|
+
// Adding the 1st entry, which is a reply, to required list
|
|
424
|
+
required.push(entry.content.items[0].item.itemContent.tweet_results?.result);
|
|
425
|
+
tweets.push(entry.content.items[0].item.itemContent.tweet_results?.result);
|
|
426
|
+
users.push(entry.content.items[0].item.itemContent.tweet_results?.result.core.user_results.result);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Iterating over the rest of the conversation
|
|
430
|
+
//@ts-ignore
|
|
431
|
+
entry.content.items.forEach(item => {
|
|
432
|
+
// If item is of type tweet
|
|
433
|
+
if (item.entryId.indexOf('tweet') != -1) {
|
|
434
|
+
// If tweet exists
|
|
435
|
+
if(item.item.itemContent.tweet_results?.result.__typename === 'Tweet') {
|
|
436
|
+
tweets.push(item.item.itemContent.tweet_results.result);
|
|
437
|
+
users.push(item.item.itemContent.tweet_results.result.core.user_results.result);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// If entry is of type bottom cursor
|
|
443
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
444
|
+
cursor = entry.content.itemContent?.value ?? '';
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Returning the data
|
|
449
|
+
return {
|
|
450
|
+
required: required,
|
|
451
|
+
cursor: cursor,
|
|
452
|
+
users: users,
|
|
453
|
+
tweets: tweets
|
|
454
|
+
};
|
|
455
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// TYPES
|
|
2
|
+
import { GuestCredentials, AuthCredentials } from '../../types/Authentication';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns The header required for making authorized HTTP requests
|
|
6
|
+
* @param authToken The authentication token received from Twitter
|
|
7
|
+
* @param csrfToken The csrf token received from Twitter
|
|
8
|
+
* @param cookie The cookie associated with the logged in account
|
|
9
|
+
*/
|
|
10
|
+
export function authorizedHeader(authCred: AuthCredentials): any {
|
|
11
|
+
return [
|
|
12
|
+
`sec-ch-ua: "Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"`,
|
|
13
|
+
`x-twitter-client-language: en`,
|
|
14
|
+
`x-csrf-token: ${authCred.csrfToken}`,
|
|
15
|
+
`sec-ch-ua-mobile: ?0`,
|
|
16
|
+
`authorization: ${authCred.authToken}`,
|
|
17
|
+
`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61`,
|
|
18
|
+
`x-twitter-auth-type: OAuth2Session`,
|
|
19
|
+
`x-twitter-active-user: yes`,
|
|
20
|
+
`sec-ch-ua-platform: "Windows"`,
|
|
21
|
+
`Accept: */*`,
|
|
22
|
+
`host: api.twitter.com`,
|
|
23
|
+
`Cookie: ${authCred.cookie}`
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function guestHeader(guestCred: GuestCredentials): any {
|
|
28
|
+
return [
|
|
29
|
+
'authority: api.twitter.com',
|
|
30
|
+
'accept: */*',
|
|
31
|
+
'accept-language: en-US,en;q=0.9',
|
|
32
|
+
`authorization: ${guestCred.authToken}`,
|
|
33
|
+
'content-type: application/json',
|
|
34
|
+
'origin: https://twitter.com',
|
|
35
|
+
'referer: https://twitter.com/',
|
|
36
|
+
'sec-ch-ua: ^\^"Not_A Brand^\^";v=^\^"99^\^", ^\^"Microsoft Edge^\^";v=^\^"109^\^", ^\^"Chromium^\^";v=^\^"109^\^"',
|
|
37
|
+
'sec-ch-ua-mobile: ?0',
|
|
38
|
+
'sec-ch-ua-platform: ^\^"Windows^\^"',
|
|
39
|
+
'sec-fetch-dest: empty',
|
|
40
|
+
'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70',
|
|
41
|
+
`x-guest-token: ${guestCred.guestToken}`,
|
|
42
|
+
'x-twitter-active-user: yes',
|
|
43
|
+
'x-twitter-client-language: en'
|
|
44
|
+
];
|
|
45
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { TweetFilter } from '../../types/Tweet';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns Whether the given json object is empty or not
|
|
5
|
+
* @param data The input JSON object which needs to be checked
|
|
6
|
+
*/
|
|
7
|
+
export function isJSONEmpty(data: any): boolean {
|
|
8
|
+
// If the JSON has any keys, it's not empty
|
|
9
|
+
if(Object.keys(data).length == 0) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
// Else, it's empty
|
|
13
|
+
else {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns The value associated with the given key inside the given json
|
|
20
|
+
* @param data The json data within which to search for the value
|
|
21
|
+
* @param key The key to search for
|
|
22
|
+
* @param last Whether to begin searching from the end
|
|
23
|
+
*/
|
|
24
|
+
export function findJSONKey(data: any, key: string, last: boolean = false): any {
|
|
25
|
+
let jsonStr: string = JSON.stringify(data); // To store the input data as string
|
|
26
|
+
let extStr: string = ''; // To store the extracted string
|
|
27
|
+
let len: number = jsonStr.length; // To store length of input data
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Getting the position to start extracting data from
|
|
31
|
+
* This the position just after the key plus ":"
|
|
32
|
+
* */
|
|
33
|
+
let start: number = !last ? (jsonStr.indexOf(`"${key}"`) + `"${key}":`.length) : (jsonStr.lastIndexOf(`"${key}"`) + `"${key}":`.length);
|
|
34
|
+
|
|
35
|
+
for (let i = start; i < len; i++) {
|
|
36
|
+
// Getting each character
|
|
37
|
+
let char: string = jsonStr[i];
|
|
38
|
+
|
|
39
|
+
// If not ending of value
|
|
40
|
+
if (char != ',' && char != '\n') {
|
|
41
|
+
extStr += char;
|
|
42
|
+
}
|
|
43
|
+
// If ending of value
|
|
44
|
+
else {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Removing begginning and ending quotes from string
|
|
50
|
+
/**
|
|
51
|
+
* I don't know how this regex I used works. I just copied it from StackOverflow
|
|
52
|
+
* Here is the link to the thread: https://stackoverflow.com/q/19156148
|
|
53
|
+
*/
|
|
54
|
+
extStr = extStr.replace(/^"|"$/g, '');
|
|
55
|
+
|
|
56
|
+
return extStr;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns A list of data from a singleton data
|
|
61
|
+
* @param data The data to be converted to a list
|
|
62
|
+
*/
|
|
63
|
+
export function dataToList(data: any | any[]): any[] {
|
|
64
|
+
// If data is already a list
|
|
65
|
+
if (Array.isArray(data)) {
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
// If data is not array
|
|
69
|
+
else {
|
|
70
|
+
return [data];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param text The text to be normalized
|
|
76
|
+
* @returns The text after being formatted to remove unnecessary characters
|
|
77
|
+
*/
|
|
78
|
+
export function normalizeText(text: string): string {
|
|
79
|
+
let normalizedText: string = ''; // To store the normalized text
|
|
80
|
+
|
|
81
|
+
// Removing unnecessary full stops, and other characters
|
|
82
|
+
normalizedText = text.replace(/\n/g, '.').replace(/[.]+[\s+.\s+]+/g, '. ');
|
|
83
|
+
|
|
84
|
+
// Adding full-stop to the end if does not exist already
|
|
85
|
+
normalizedText = normalizedText.endsWith('.') ? normalizedText : (normalizedText + '.');
|
|
86
|
+
|
|
87
|
+
return normalizedText;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param filter The tweet filter to use for getting filtered tweets
|
|
92
|
+
* @returns The same tweet filter, in a URL query format string
|
|
93
|
+
*/
|
|
94
|
+
export function toQueryString(filter: TweetFilter): string {
|
|
95
|
+
// Concatenating the input filter arguments to a URL query formatted string
|
|
96
|
+
return [
|
|
97
|
+
filter.words ? filter.words.join(' ') : '',
|
|
98
|
+
filter.hashtags ? `(${filter.hashtags.map(hashtag => '%23' + hashtag).join(' OR ')})` : '',
|
|
99
|
+
filter.fromUsers ? `(${filter.fromUsers.map(user => `from:${user}`).join(' OR ')})` : '',
|
|
100
|
+
filter.toUsers ? `(${filter.toUsers.map(user => `to:${user}`).join(' OR ')})` : '',
|
|
101
|
+
filter.mentions ? `(${filter.mentions.map(mention => '%40' + mention).join(' OR ')})` : '',
|
|
102
|
+
filter.startDate ? `since:${filter.startDate}` : '',
|
|
103
|
+
filter.endDate ? `until:${filter.endDate}` : '',
|
|
104
|
+
filter.quoted ? `quoted_tweet_id:${filter.quoted}` : ''
|
|
105
|
+
]
|
|
106
|
+
.filter(item => item !== '()' && item !== '')
|
|
107
|
+
.join(' ') + (!filter.links ? '%20-filter%3Alinks' : '');
|
|
108
|
+
}
|