mirror of
https://github.com/CompeyDev/fxtwitter-docker.git
synced 2025-05-04 10:43:49 +01:00
Merge pull request #232 from Wazbat/feature/user_profiles
✨ Added support for fetching user profiles
This commit is contained in:
commit
27bfad0213
9 changed files with 442 additions and 91 deletions
|
@ -32,6 +32,7 @@ const populateTweetProperties = async (
|
||||||
apiTweet.id = tweet.id_str;
|
apiTweet.id = tweet.id_str;
|
||||||
apiTweet.text = unescapeText(linkFixer(tweet, tweet.full_text || ''));
|
apiTweet.text = unescapeText(linkFixer(tweet, tweet.full_text || ''));
|
||||||
apiTweet.author = {
|
apiTweet.author = {
|
||||||
|
id: tweet.user_id_str,
|
||||||
name: name,
|
name: name,
|
||||||
screen_name: screenName,
|
screen_name: screenName,
|
||||||
avatar_url:
|
avatar_url:
|
||||||
|
@ -178,7 +179,7 @@ export const statusAPI = async (
|
||||||
language: string | undefined,
|
language: string | undefined,
|
||||||
event: FetchEvent,
|
event: FetchEvent,
|
||||||
flags?: InputFlags
|
flags?: InputFlags
|
||||||
): Promise<APIResponse> => {
|
): Promise<TweetAPIResponse> => {
|
||||||
let conversation = await fetchConversation(status, event);
|
let conversation = await fetchConversation(status, event);
|
||||||
let tweet = conversation?.globalObjects?.tweets?.[status] || {};
|
let tweet = conversation?.globalObjects?.tweets?.[status] || {};
|
||||||
|
|
||||||
|
@ -233,7 +234,7 @@ export const statusAPI = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Creating the response objects */
|
/* Creating the response objects */
|
||||||
const response: APIResponse = { code: 200, message: 'OK' } as APIResponse;
|
const response: TweetAPIResponse = { code: 200, message: 'OK' } as TweetAPIResponse;
|
||||||
const apiTweet: APITweet = (await populateTweetProperties(
|
const apiTweet: APITweet = (await populateTweetProperties(
|
||||||
tweet,
|
tweet,
|
||||||
conversation,
|
conversation,
|
||||||
|
|
74
src/api/user.ts
Normal file
74
src/api/user.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { Constants } from '../constants';
|
||||||
|
import { fetchUser } from '../fetch';
|
||||||
|
|
||||||
|
/* This function does the heavy lifting of processing data from Twitter API
|
||||||
|
and using it to create FixTweet's streamlined API responses */
|
||||||
|
const populateUserProperties = async (
|
||||||
|
response: GraphQLUserResponse
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): Promise<APIUser> => {
|
||||||
|
const apiUser = {} as APIUser;
|
||||||
|
|
||||||
|
const user = response.data.user.result;
|
||||||
|
/* Populating a lot of the basics */
|
||||||
|
apiUser.url = `${Constants.TWITTER_ROOT}/${user.legacy.screen_name}`;
|
||||||
|
apiUser.id = user.rest_id;
|
||||||
|
apiUser.followers = user.legacy.followers_count;
|
||||||
|
apiUser.following = user.legacy.friends_count;
|
||||||
|
apiUser.likes = user.legacy.favourites_count;
|
||||||
|
apiUser.tweets = user.legacy.statuses_count;
|
||||||
|
apiUser.name = user.legacy.name;
|
||||||
|
apiUser.screen_name = user.legacy.screen_name;
|
||||||
|
apiUser.description = user.legacy.description;
|
||||||
|
apiUser.location = user.legacy.location;
|
||||||
|
/*
|
||||||
|
if (user.is_blue_verified) {
|
||||||
|
apiUser.verified = 'blue';
|
||||||
|
} else if (user.legacy.verified) {
|
||||||
|
if (user.legacy.verified_type === 'Business') {
|
||||||
|
apiUser.verified = 'business';
|
||||||
|
} else if (user.legacy.verified_type === 'Government') {
|
||||||
|
apiUser.verified = 'government';
|
||||||
|
} else {
|
||||||
|
apiUser.verified = 'legacy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiUser.verified === 'government') {
|
||||||
|
apiUser.verified_label = user.affiliates_highlighted_label?.label?.description || '';
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
apiUser.avatar_url = user.legacy.profile_image_url_https;
|
||||||
|
apiUser.joined = user.legacy.created_at;
|
||||||
|
if (user.legacy_extended_profile?.birthdate) {
|
||||||
|
const { birthdate } = user.legacy_extended_profile;
|
||||||
|
apiUser.birthday = {};
|
||||||
|
if (typeof birthdate.day === 'number') apiUser.birthday.day = birthdate.day;
|
||||||
|
if (typeof birthdate.month === 'number') apiUser.birthday.month = birthdate.month;
|
||||||
|
if (typeof birthdate.year === 'number') apiUser.birthday.year = birthdate.year;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* API for Twitter profiles (Users)
|
||||||
|
Used internally by FixTweet's embed service, or
|
||||||
|
available for free using api.fxtwitter.com. */
|
||||||
|
export const userAPI = async (
|
||||||
|
username: string,
|
||||||
|
event: FetchEvent,
|
||||||
|
flags?: InputFlags
|
||||||
|
): Promise<UserAPIResponse> => {
|
||||||
|
const userResponse = await fetchUser(username, event);
|
||||||
|
|
||||||
|
/* Creating the response objects */
|
||||||
|
const response: UserAPIResponse = { code: 200, message: 'OK' } as UserAPIResponse;
|
||||||
|
const apiUser: APIUser = (await populateUserProperties(
|
||||||
|
userResponse,
|
||||||
|
)) as APIUser;
|
||||||
|
|
||||||
|
/* Finally, staple the User to the response and return it */
|
||||||
|
response.user = apiUser;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
39
src/fetch.ts
39
src/fetch.ts
|
@ -203,3 +203,42 @@ export const fetchConversation = async (
|
||||||
}
|
}
|
||||||
)) as TimelineBlobPartial;
|
)) as TimelineBlobPartial;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchUser = async (
|
||||||
|
username: string,
|
||||||
|
event: FetchEvent,
|
||||||
|
useElongator = false
|
||||||
|
): Promise<GraphQLUserResponse> => {
|
||||||
|
return (await twitterFetch(
|
||||||
|
`${Constants.TWITTER_ROOT}/i/api/graphql/sLVLhk0bGj3MVFEKTdax1w/UserByScreenName?variables=${
|
||||||
|
encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
screen_name: username,
|
||||||
|
withSafetyModeUserFields: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}&features=${encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
blue_business_profile_image_shape_enabled: true,
|
||||||
|
responsive_web_graphql_exclude_directive_enabled: true,
|
||||||
|
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||||
|
responsive_web_graphql_timeline_navigation_enabled: false,
|
||||||
|
verified_phone_label_enabled: true
|
||||||
|
})
|
||||||
|
)}`,
|
||||||
|
event,
|
||||||
|
useElongator,
|
||||||
|
// Validator function
|
||||||
|
(_res: unknown) => {
|
||||||
|
const response = _res as GraphQLUserResponse;
|
||||||
|
return !(response?.data?.user?.result?.__typename !== 'User' || typeof response.data.user.result.legacy === 'undefined');
|
||||||
|
/*
|
||||||
|
return !(
|
||||||
|
typeof conversation.globalObjects === 'undefined' &&
|
||||||
|
(typeof conversation.errors === 'undefined' ||
|
||||||
|
conversation.errors?.[0]?.code === 239)
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
)) as GraphQLUserResponse;
|
||||||
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Strings } from './strings';
|
||||||
|
|
||||||
import motd from '../motd.json';
|
import motd from '../motd.json';
|
||||||
import { sanitizeText } from './helpers/utils';
|
import { sanitizeText } from './helpers/utils';
|
||||||
|
import { handleProfile } from './user';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
@ -140,17 +141,90 @@ const statusRequest = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Redirects to user profile when linked.
|
/* Handler for User Profiles */
|
||||||
We don't do any fancy special embeds yet, just Twitter default embeds. */
|
const profileRequest = async (request: IRequest, event: FetchEvent,
|
||||||
const profileRequest = async (request: IRequest) => {
|
flags: InputFlags = {}) => {
|
||||||
const { handle } = request.params;
|
const { handle } = request.params;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
const userAgent = request.headers.get('User-Agent') || '';
|
||||||
|
|
||||||
|
/* User Agent matching for embed generators, bots, crawlers, and other automated
|
||||||
|
tools. It's pretty all-encompassing. Note that Firefox/92 is in here because
|
||||||
|
Discord sometimes uses the following UA:
|
||||||
|
|
||||||
|
Mozilla/5.0 (Macintosh; Intel Mac OS X 11.6; rv:92.0) Gecko/20100101 Firefox/92.0
|
||||||
|
|
||||||
|
I'm not sure why that specific one, it's pretty weird, but this edge case ensures
|
||||||
|
stuff keeps working.
|
||||||
|
|
||||||
|
On the very rare off chance someone happens to be using specifically Firefox 92,
|
||||||
|
the http-equiv="refresh" meta tag will ensure an actual human is sent to the destination. */
|
||||||
|
const isBotUA = userAgent.match(Constants.BOT_UA_REGEX) !== null;
|
||||||
|
|
||||||
/* If not a valid screen name, we redirect to project GitHub */
|
/* If not a valid screen name, we redirect to project GitHub */
|
||||||
if (handle.match(/\w{1,15}/gi)?.[0] !== handle) {
|
if (handle.match(/\w{1,15}/gi)?.[0] !== handle) {
|
||||||
return Response.redirect(Constants.REDIRECT_URL, 302);
|
return Response.redirect(Constants.REDIRECT_URL, 302);
|
||||||
|
}
|
||||||
|
const username = handle.match(/\w{1,15}/gi)?.[0] as string;
|
||||||
|
/* Check if request is to api.fxtwitter.com */
|
||||||
|
if (Constants.API_HOST_LIST.includes(url.hostname)) {
|
||||||
|
console.log('JSON API request');
|
||||||
|
flags.api = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct media or API access bypasses bot check, returning same response regardless of UA */
|
||||||
|
if (isBotUA || flags.api) {
|
||||||
|
if (isBotUA) {
|
||||||
|
console.log(`Matched bot UA ${userAgent}`);
|
||||||
} else {
|
} else {
|
||||||
return Response.redirect(`${Constants.TWITTER_ROOT}${url.pathname}`, 302);
|
console.log('Bypass bot check');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This throws the necessary data to handleStatus (in status.ts) */
|
||||||
|
const profileResponse = await handleProfile(
|
||||||
|
username,
|
||||||
|
userAgent,
|
||||||
|
flags,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Complete responses are normally sent just by errors. Normal embeds send a `text` value. */
|
||||||
|
if (profileResponse.response) {
|
||||||
|
console.log('handleProfile sent response');
|
||||||
|
return profileResponse.response;
|
||||||
|
} else if (profileResponse.text) {
|
||||||
|
console.log('handleProfile sent embed');
|
||||||
|
/* TODO This check has purpose in the original handleStatus handler, but I'm not sure if this edge case can happen here */
|
||||||
|
if (!isBotUA) {
|
||||||
|
return Response.redirect(`${Constants.TWITTER_ROOT}/${handle}`, 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = Constants.RESPONSE_HEADERS;
|
||||||
|
|
||||||
|
if (profileResponse.cacheControl) {
|
||||||
|
headers = { ...headers, 'cache-control': profileResponse.cacheControl };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the response containing embed information */
|
||||||
|
return new Response(profileResponse.text, {
|
||||||
|
headers: headers,
|
||||||
|
status: 200
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
/* Somehow handleStatus sent us nothing. This should *never* happen, but we have a case for it. */
|
||||||
|
return new Response(Strings.ERROR_UNKNOWN, {
|
||||||
|
headers: Constants.RESPONSE_HEADERS,
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* A human has clicked a fxtwitter.com/:screen_name link!
|
||||||
|
Obviously we just need to redirect to the user directly.*/
|
||||||
|
console.log('Matched human UA', userAgent);
|
||||||
|
return Response.redirect(
|
||||||
|
`${Constants.TWITTER_ROOT}/${handle}`,
|
||||||
|
302
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,7 @@ This is caused by Twitter API downtime or a new bug. Try again in a little while
|
||||||
ERROR_API_FAIL: 'Tweet failed to load due to an API error :(',
|
ERROR_API_FAIL: 'Tweet failed to load due to an API error :(',
|
||||||
ERROR_PRIVATE: `Sorry, we can't embed this Tweet because the user is private or suspended :(`,
|
ERROR_PRIVATE: `Sorry, we can't embed this Tweet because the user is private or suspended :(`,
|
||||||
ERROR_TWEET_NOT_FOUND: `Sorry, that Tweet doesn't exist :(`,
|
ERROR_TWEET_NOT_FOUND: `Sorry, that Tweet doesn't exist :(`,
|
||||||
|
ERROR_USER_NOT_FOUND: `Sorry, that user doesn't exist :(`,
|
||||||
ERROR_UNKNOWN: `Unknown error occurred, sorry about that :(`,
|
ERROR_UNKNOWN: `Unknown error occurred, sorry about that :(`,
|
||||||
|
|
||||||
TWITFIX_API_SUNSET: `The original TwitFix API has been sunset. To learn more about the FixTweet API, check out <a href="https://${
|
TWITFIX_API_SUNSET: `The original TwitFix API has been sunset. To learn more about the FixTweet API, check out <a href="https://${
|
||||||
|
|
112
src/types/twitterTypes.d.ts
vendored
112
src/types/twitterTypes.d.ts
vendored
|
@ -193,3 +193,115 @@ type TranslationPartial = {
|
||||||
translation: string;
|
translation: string;
|
||||||
entities: TweetEntities;
|
entities: TweetEntities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GraphQLUserResponse = {
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
result: GraphQLUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GraphQLUser = {
|
||||||
|
__typename: "User";
|
||||||
|
id: string; // "VXNlcjo3ODMyMTQ="
|
||||||
|
rest_id: string; // "783214",
|
||||||
|
affiliates_highlighted_label: {
|
||||||
|
label?: {
|
||||||
|
badge?: {
|
||||||
|
url?: string; // "https://pbs.twimg.com/semantic_core_img/1290392753013002240/mWq1iE5L?format=png&name=orig"
|
||||||
|
}
|
||||||
|
description?: string; // "United States government organization"
|
||||||
|
url?: {
|
||||||
|
url?: string; // "https://help.twitter.com/rules-and-policies/state-affiliated"
|
||||||
|
urlType: string; // "DeepLink"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
business_account: {
|
||||||
|
affiliates_count?: 20
|
||||||
|
}
|
||||||
|
is_blue_verified: boolean; // false,
|
||||||
|
profile_image_shape: 'Circle' | 'Square'|'Hexagon'; // "Circle",
|
||||||
|
has_nft_avatar: boolean; // false,
|
||||||
|
legacy: {
|
||||||
|
created_at: string; // "Tue Feb 20 14:35:54 +0000 2007",
|
||||||
|
default_profile: boolean // false,
|
||||||
|
default_profile_image: boolean // false,
|
||||||
|
description: string; // "What's happening?!",
|
||||||
|
entities: {
|
||||||
|
description?: {
|
||||||
|
urls?: {
|
||||||
|
display_url: string; // "about.twitter.com",
|
||||||
|
expanded_url: string; // "https://about.twitter.com/",
|
||||||
|
url: string; // "https://t.co/DAtOo6uuHk",
|
||||||
|
indices: [
|
||||||
|
0,
|
||||||
|
23
|
||||||
|
]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fast_followers_count: 0,
|
||||||
|
favourites_count: number; // 126708,
|
||||||
|
followers_count: number; // 4996,
|
||||||
|
friends_count: number; // 2125,
|
||||||
|
has_custom_timelines: boolean; // true,
|
||||||
|
is_translator: boolean; // false,
|
||||||
|
listed_count: number; // 88165,
|
||||||
|
location: string; // "everywhere",
|
||||||
|
media_count: number; // 20839,
|
||||||
|
name: string; // "Twitter",
|
||||||
|
normal_followers_count: number; // 65669107,
|
||||||
|
pinned_tweet_ids_str: string[]; // Array of tweet ids, usually one. Empty if no pinned tweet
|
||||||
|
possibly_sensitive: boolean; // false,
|
||||||
|
profile_banner_url: string; // "https://pbs.twimg.com/profile_banners/783214/1646075315",
|
||||||
|
profile_image_url_https: string; // "https://pbs.twimg.com/profile_images/1488548719062654976/u6qfBBkF_normal.jpg",
|
||||||
|
profile_interstitial_type: string; // "",
|
||||||
|
screen_name: string; // "Twitter",
|
||||||
|
statuses_count: number; // 15047
|
||||||
|
translator_type: string; // "regular"
|
||||||
|
verified: boolean; // false
|
||||||
|
verified_type: 'Business'|'Government';
|
||||||
|
withheld_in_countries: []
|
||||||
|
},
|
||||||
|
professional: {
|
||||||
|
rest_id: string; // "1503055759638159366",
|
||||||
|
professional_type: string; // "Creator",
|
||||||
|
category: [
|
||||||
|
{
|
||||||
|
id: number; // 354,
|
||||||
|
name: string // "Community",
|
||||||
|
icon_name: string; // "IconBriefcaseStroke"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
legacy_extended_profile: {
|
||||||
|
birthdate?: {
|
||||||
|
day: number; // 7,
|
||||||
|
month: number; // 1,
|
||||||
|
visibility: string; // "Public"
|
||||||
|
year: number; // 2000
|
||||||
|
year_visibility: string; // "Public"
|
||||||
|
};
|
||||||
|
profile_image_shape: string; // "Circle",
|
||||||
|
rest_id: string; // "783214",
|
||||||
|
},
|
||||||
|
is_profile_translatable: false,
|
||||||
|
verification_info: {
|
||||||
|
reason: {
|
||||||
|
description: {
|
||||||
|
entities: {
|
||||||
|
from_index: number; // 98,
|
||||||
|
ref: {
|
||||||
|
url: string; // "https://help.twitter.com/managing-your-account/about-twitter-verified-accounts",
|
||||||
|
url_type: string; // "ExternalUrl"
|
||||||
|
};
|
||||||
|
to_index: number; // 108
|
||||||
|
}[];
|
||||||
|
text?: 'This account is verified because it’s subscribed to Twitter Blue or is a legacy verified account. Learn more'|'This account is verified because it\'s an official organisation on Twitter. Learn more';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/types/types.d.ts
vendored
37
src/types/types.d.ts
vendored
|
@ -40,26 +40,34 @@ interface VerticalSize {
|
||||||
secondHeight: number;
|
secondHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIResponse {
|
interface TweetAPIResponse {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
tweet?: APITweet;
|
tweet?: APITweet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserAPIResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
user?: APIUser;
|
||||||
|
}
|
||||||
|
|
||||||
interface APITranslate {
|
interface APITranslate {
|
||||||
text: string;
|
text: string;
|
||||||
source_lang: string;
|
source_lang: string;
|
||||||
source_lang_en: string;
|
source_lang_en: string;
|
||||||
target_lang: string;
|
target_lang: string;
|
||||||
}
|
}
|
||||||
|
interface BaseUser {
|
||||||
interface APIAuthor {
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
screen_name?: string;
|
screen_name?: string;
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
avatar_color: string;
|
|
||||||
banner_url?: string;
|
banner_url?: string;
|
||||||
}
|
}
|
||||||
|
interface APITweetAuthor extends BaseUser {
|
||||||
|
avatar_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface APIExternalMedia {
|
interface APIExternalMedia {
|
||||||
type: 'video';
|
type: 'video';
|
||||||
|
@ -124,7 +132,7 @@ interface APITweet {
|
||||||
quote?: APITweet;
|
quote?: APITweet;
|
||||||
poll?: APIPoll;
|
poll?: APIPoll;
|
||||||
translation?: APITranslate;
|
translation?: APITranslate;
|
||||||
author: APIAuthor;
|
author: APITweetAuthor;
|
||||||
|
|
||||||
media?: {
|
media?: {
|
||||||
external?: APIExternalMedia;
|
external?: APIExternalMedia;
|
||||||
|
@ -141,3 +149,22 @@ interface APITweet {
|
||||||
|
|
||||||
twitter_card: 'tweet' | 'summary' | 'summary_large_image' | 'player';
|
twitter_card: 'tweet' | 'summary' | 'summary_large_image' | 'player';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface APIUser extends BaseUser {
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
url: string;
|
||||||
|
protected: boolean;
|
||||||
|
// verified: 'legacy' | 'blue'| 'business' | 'government';
|
||||||
|
// verified_label: string;
|
||||||
|
followers: number;
|
||||||
|
following: number;
|
||||||
|
tweets: number;
|
||||||
|
likes: number;
|
||||||
|
joined: string;
|
||||||
|
birthday: {
|
||||||
|
day?: number;
|
||||||
|
month?: number;
|
||||||
|
year?: number
|
||||||
|
}
|
||||||
|
}
|
65
src/user.ts
Normal file
65
src/user.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Constants } from './constants';
|
||||||
|
import { Strings } from './strings';
|
||||||
|
import { userAPI } from './api/user';
|
||||||
|
|
||||||
|
export const returnError = (error: string): StatusResponse => {
|
||||||
|
return {
|
||||||
|
text: Strings.BASE_HTML.format({
|
||||||
|
lang: '',
|
||||||
|
headers: [
|
||||||
|
`<meta property="og:title" content="${Constants.BRANDING_NAME}"/>`,
|
||||||
|
`<meta property="og:description" content="${error}"/>`
|
||||||
|
].join('')
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Handler for Twitter users */
|
||||||
|
export const handleProfile = async (
|
||||||
|
username: string,
|
||||||
|
userAgent?: string,
|
||||||
|
flags?: InputFlags,
|
||||||
|
event?: FetchEvent
|
||||||
|
): Promise<StatusResponse> => {
|
||||||
|
console.log('Direct?', flags?.direct);
|
||||||
|
|
||||||
|
const api = await userAPI(username, event as FetchEvent);
|
||||||
|
const user = api?.user as APIUser;
|
||||||
|
|
||||||
|
/* Catch this request if it's an API response */
|
||||||
|
// For now we just always return the API response while testing
|
||||||
|
if (flags?.api) {
|
||||||
|
return {
|
||||||
|
response: new Response(JSON.stringify(api), {
|
||||||
|
headers: { ...Constants.RESPONSE_HEADERS, ...Constants.API_RESPONSE_HEADERS },
|
||||||
|
status: api.code
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If there was any errors fetching the User, we'll return it */
|
||||||
|
switch (api.code) {
|
||||||
|
case 401:
|
||||||
|
return returnError(Strings.ERROR_PRIVATE);
|
||||||
|
case 404:
|
||||||
|
return returnError(Strings.ERROR_USER_NOT_FOUND);
|
||||||
|
case 500:
|
||||||
|
return returnError(Strings.ERROR_API_FAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base headers included in all responses */
|
||||||
|
const headers = [
|
||||||
|
`<meta property="twitter:site" content="@${user.screen_name}"/>`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO Add card creation logic here
|
||||||
|
|
||||||
|
/* Finally, after all that work we return the response HTML! */
|
||||||
|
return {
|
||||||
|
text: Strings.BASE_HTML.format({
|
||||||
|
lang: `lang="en"`,
|
||||||
|
headers: headers.join('')
|
||||||
|
}),
|
||||||
|
cacheControl: null
|
||||||
|
};
|
||||||
|
};
|
|
@ -71,7 +71,7 @@ test('API fetch basic Tweet', async () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(result.status).toEqual(200);
|
expect(result.status).toEqual(200);
|
||||||
const response = (await result.json()) as APIResponse;
|
const response = (await result.json()) as TweetAPIResponse;
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
expect(response.code).toEqual(200);
|
expect(response.code).toEqual(200);
|
||||||
expect(response.message).toEqual('OK');
|
expect(response.message).toEqual('OK');
|
||||||
|
@ -82,6 +82,7 @@ test('API fetch basic Tweet', async () => {
|
||||||
expect(tweet.id).toEqual('20');
|
expect(tweet.id).toEqual('20');
|
||||||
expect(tweet.text).toEqual('just setting up my twttr');
|
expect(tweet.text).toEqual('just setting up my twttr');
|
||||||
expect(tweet.author.screen_name?.toLowerCase()).toEqual('jack');
|
expect(tweet.author.screen_name?.toLowerCase()).toEqual('jack');
|
||||||
|
expect(tweet.author.id).toEqual('12');
|
||||||
expect(tweet.author.name).toBeTruthy();
|
expect(tweet.author.name).toBeTruthy();
|
||||||
expect(tweet.author.avatar_url).toBeTruthy();
|
expect(tweet.author.avatar_url).toBeTruthy();
|
||||||
expect(tweet.author.banner_url).toBeTruthy();
|
expect(tweet.author.banner_url).toBeTruthy();
|
||||||
|
@ -104,7 +105,7 @@ test('API fetch video Tweet', async () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(result.status).toEqual(200);
|
expect(result.status).toEqual(200);
|
||||||
const response = (await result.json()) as APIResponse;
|
const response = (await result.json()) as TweetAPIResponse;
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
expect(response.code).toEqual(200);
|
expect(response.code).toEqual(200);
|
||||||
expect(response.message).toEqual('OK');
|
expect(response.message).toEqual('OK');
|
||||||
|
@ -117,6 +118,7 @@ test('API fetch video Tweet', async () => {
|
||||||
'Get the sauces ready, #NuggsForCarter has 3 million+ Retweets.'
|
'Get the sauces ready, #NuggsForCarter has 3 million+ Retweets.'
|
||||||
);
|
);
|
||||||
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
||||||
|
expect(tweet.author.id).toEqual('783214');
|
||||||
expect(tweet.author.name).toBeTruthy();
|
expect(tweet.author.name).toBeTruthy();
|
||||||
expect(tweet.author.avatar_url).toBeTruthy();
|
expect(tweet.author.avatar_url).toBeTruthy();
|
||||||
expect(tweet.author.banner_url).toBeTruthy();
|
expect(tweet.author.banner_url).toBeTruthy();
|
||||||
|
@ -149,7 +151,7 @@ test('API fetch multi-photo Tweet', async () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(result.status).toEqual(200);
|
expect(result.status).toEqual(200);
|
||||||
const response = (await result.json()) as APIResponse;
|
const response = (await result.json()) as TweetAPIResponse;
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
expect(response.code).toEqual(200);
|
expect(response.code).toEqual(200);
|
||||||
expect(response.message).toEqual('OK');
|
expect(response.message).toEqual('OK');
|
||||||
|
@ -160,6 +162,7 @@ test('API fetch multi-photo Tweet', async () => {
|
||||||
expect(tweet.id).toEqual('1445094085593866246');
|
expect(tweet.id).toEqual('1445094085593866246');
|
||||||
expect(tweet.text).toEqual('@netflix');
|
expect(tweet.text).toEqual('@netflix');
|
||||||
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
||||||
|
expect(tweet.author.id).toEqual('783214');
|
||||||
expect(tweet.author.name).toBeTruthy();
|
expect(tweet.author.name).toBeTruthy();
|
||||||
expect(tweet.author.avatar_url).toBeTruthy();
|
expect(tweet.author.avatar_url).toBeTruthy();
|
||||||
expect(tweet.author.banner_url).toBeTruthy();
|
expect(tweet.author.banner_url).toBeTruthy();
|
||||||
|
@ -188,82 +191,6 @@ test('API fetch multi-photo Tweet', async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// test('API fetch multi-video Tweet', async () => {
|
|
||||||
// const result = await cacheWrapper(
|
|
||||||
// new Request('https://api.fxtwitter.com/dangeredwolf/status/1557914172763127808', {
|
|
||||||
// method: 'GET',
|
|
||||||
// headers: botHeaders
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// expect(result.status).toEqual(200);
|
|
||||||
// const response = (await result.json()) as APIResponse;
|
|
||||||
// expect(response).toBeTruthy();
|
|
||||||
// expect(response.code).toEqual(200);
|
|
||||||
// expect(response.message).toEqual('OK');
|
|
||||||
|
|
||||||
// const tweet = response.tweet as APITweet;
|
|
||||||
// expect(tweet).toBeTruthy();
|
|
||||||
// expect(tweet.url).toEqual(
|
|
||||||
// 'https://twitter.com/dangeredwolf/status/1557914172763127808'
|
|
||||||
// );
|
|
||||||
// expect(tweet.id).toEqual('1557914172763127808');
|
|
||||||
// expect(tweet.text).toEqual('');
|
|
||||||
// expect(tweet.author.screen_name?.toLowerCase()).toEqual('dangeredwolf');
|
|
||||||
// expect(tweet.author.name).toBeTruthy();
|
|
||||||
// expect(tweet.author.avatar_url).toBeTruthy();
|
|
||||||
// expect(tweet.author.banner_url).toBeTruthy();
|
|
||||||
// expect(tweet.author.avatar_color).toBeTruthy();
|
|
||||||
// expect(tweet.twitter_card).toEqual('player');
|
|
||||||
// expect(tweet.created_at).toEqual('Fri Aug 12 02:17:38 +0000 2022');
|
|
||||||
// expect(tweet.created_timestamp).toEqual(1660270658);
|
|
||||||
// expect(tweet.replying_to).toBeNull();
|
|
||||||
// expect(tweet.media?.videos).toBeTruthy();
|
|
||||||
// const videos = tweet.media?.videos as APIVideo[];
|
|
||||||
// expect(videos[0].url).toEqual(
|
|
||||||
// 'https://video.twimg.com/ext_tw_video/1539029945124528130/pu/vid/1662x1080/ZQP4eoQhnGnKcLEb.mp4?tag=14'
|
|
||||||
// );
|
|
||||||
// expect(videos[0].thumbnail_url).toEqual(
|
|
||||||
// 'https://pbs.twimg.com/ext_tw_video_thumb/1539029945124528130/pu/img/6Z1MXMliums60j03.jpg'
|
|
||||||
// );
|
|
||||||
// expect(videos[0].width).toEqual(3548);
|
|
||||||
// expect(videos[0].height).toEqual(2304);
|
|
||||||
// expect(videos[0].duration).toEqual(37.75);
|
|
||||||
// expect(videos[0].format).toEqual('video/mp4');
|
|
||||||
// expect(videos[0].type).toEqual('video');
|
|
||||||
// expect(videos[1].url).toEqual(
|
|
||||||
// 'https://video.twimg.com/ext_tw_video/1543316856697769984/pu/vid/1920x1080/3fo7b4EnWv2WO8Z1.mp4?tag=14'
|
|
||||||
// );
|
|
||||||
// expect(videos[1].thumbnail_url).toEqual(
|
|
||||||
// 'https://pbs.twimg.com/ext_tw_video_thumb/1543316856697769984/pu/img/eCl67JRWO8r4r8A4.jpg'
|
|
||||||
// );
|
|
||||||
// expect(videos[1].width).toEqual(1920);
|
|
||||||
// expect(videos[1].height).toEqual(1080);
|
|
||||||
// expect(videos[1].duration).toEqual(71.855);
|
|
||||||
// expect(videos[1].format).toEqual('video/mp4');
|
|
||||||
// expect(videos[1].type).toEqual('video');
|
|
||||||
// expect(videos[2].url).toEqual(
|
|
||||||
// 'https://video.twimg.com/ext_tw_video/1543797953105625088/pu/vid/1920x1080/GHSLxzBrwiDLhLYD.mp4?tag=14'
|
|
||||||
// );
|
|
||||||
// expect(videos[2].thumbnail_url).toEqual(
|
|
||||||
// 'https://pbs.twimg.com/ext_tw_video_thumb/1543797953105625088/pu/img/2eX2QQkd7b2S1YDl.jpg'
|
|
||||||
// );
|
|
||||||
// expect(videos[2].width).toEqual(1920);
|
|
||||||
// expect(videos[2].height).toEqual(1080);
|
|
||||||
// expect(videos[2].duration).toEqual(22.018);
|
|
||||||
// expect(videos[2].format).toEqual('video/mp4');
|
|
||||||
// expect(videos[2].type).toEqual('video');
|
|
||||||
// expect(videos[3].url).toEqual(
|
|
||||||
// 'https://video.twimg.com/ext_tw_video/1548602342488129536/pu/vid/720x1280/I_D3svYfjBl7_xGS.mp4?tag=14'
|
|
||||||
// );
|
|
||||||
// expect(videos[3].thumbnail_url).toEqual(
|
|
||||||
// 'https://pbs.twimg.com/ext_tw_video_thumb/1548602342488129536/pu/img/V_1u5Nv5BwKBynwv.jpg'
|
|
||||||
// );
|
|
||||||
// expect(videos[3].width).toEqual(720);
|
|
||||||
// expect(videos[3].height).toEqual(1280);
|
|
||||||
// expect(videos[3].duration).toEqual(25.133);
|
|
||||||
// expect(videos[3].format).toEqual('video/mp4');
|
|
||||||
// expect(videos[3].type).toEqual('video');
|
|
||||||
// });
|
|
||||||
|
|
||||||
test('API fetch poll Tweet', async () => {
|
test('API fetch poll Tweet', async () => {
|
||||||
const result = await cacheWrapper(
|
const result = await cacheWrapper(
|
||||||
|
@ -273,7 +200,7 @@ test('API fetch poll Tweet', async () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(result.status).toEqual(200);
|
expect(result.status).toEqual(200);
|
||||||
const response = (await result.json()) as APIResponse;
|
const response = (await result.json()) as TweetAPIResponse;
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
expect(response.code).toEqual(200);
|
expect(response.code).toEqual(200);
|
||||||
expect(response.message).toEqual('OK');
|
expect(response.message).toEqual('OK');
|
||||||
|
@ -284,6 +211,7 @@ test('API fetch poll Tweet', async () => {
|
||||||
expect(tweet.id).toEqual('1055475950543167488');
|
expect(tweet.id).toEqual('1055475950543167488');
|
||||||
expect(tweet.text).toEqual('A poll:');
|
expect(tweet.text).toEqual('A poll:');
|
||||||
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
expect(tweet.author.screen_name?.toLowerCase()).toEqual('twitter');
|
||||||
|
expect(tweet.author.id).toEqual('783214');
|
||||||
expect(tweet.author.name).toBeTruthy();
|
expect(tweet.author.name).toBeTruthy();
|
||||||
expect(tweet.author.avatar_url).toBeTruthy();
|
expect(tweet.author.avatar_url).toBeTruthy();
|
||||||
expect(tweet.author.banner_url).toBeTruthy();
|
expect(tweet.author.banner_url).toBeTruthy();
|
||||||
|
@ -313,3 +241,33 @@ test('API fetch poll Tweet', async () => {
|
||||||
expect(choices[3].count).toEqual(31706);
|
expect(choices[3].count).toEqual(31706);
|
||||||
expect(choices[3].percentage).toEqual(58);
|
expect(choices[3].percentage).toEqual(58);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('API fetch user', async () => {
|
||||||
|
const result = await cacheWrapper(
|
||||||
|
new Request('https://api.fxtwitter.com/twitter', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: botHeaders
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(result.status).toEqual(200);
|
||||||
|
const response = (await result.json()) as UserAPIResponse;
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
expect(response.code).toEqual(200);
|
||||||
|
expect(response.message).toEqual('OK');
|
||||||
|
|
||||||
|
const user = response.user as APIUser;
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.url).toEqual('https://twitter.com/Twitter');
|
||||||
|
expect(user.id).toEqual('783214');
|
||||||
|
expect(user.screen_name).toEqual('Twitter');
|
||||||
|
expect(user.followers).toEqual(expect.any(Number));
|
||||||
|
expect(user.following).toEqual(expect.any(Number));
|
||||||
|
// The official twitter account will never be following as many people as it has followers
|
||||||
|
expect(user.following).not.toEqual(user.followers);
|
||||||
|
expect(user.likes).toEqual(expect.any(Number));
|
||||||
|
// expect(user.verified).toEqual('business');
|
||||||
|
expect(user.joined).toEqual('Tue Feb 20 14:35:54 +0000 2007');
|
||||||
|
expect(user.birthday.day).toEqual(21);
|
||||||
|
expect(user.birthday.month).toEqual(3);
|
||||||
|
expect(user.birthday.year).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue