♻️ Updated code as per code review

This commit is contained in:
Wazbat 2023-04-15 23:14:25 +02:00
parent 14e0ab7a61
commit e981745d82
7 changed files with 83 additions and 99 deletions

View file

@ -4,8 +4,7 @@ import { fetchUser } from '../fetch';
/* This function does the heavy lifting of processing data from Twitter API /* This function does the heavy lifting of processing data from Twitter API
and using it to create FixTweet's streamlined API responses */ and using it to create FixTweet's streamlined API responses */
const populateUserProperties = async ( const populateUserProperties = async (
response: GraphQLUserResponse, response: GraphQLUserResponse
language: string | undefined
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<APIUser> => { ): Promise<APIUser> => {
const apiUser = {} as APIUser; const apiUser = {} as APIUser;
@ -22,7 +21,20 @@ const populateUserProperties = async (
apiUser.screen_name = user.legacy.screen_name; apiUser.screen_name = user.legacy.screen_name;
apiUser.description = user.legacy.description; apiUser.description = user.legacy.description;
apiUser.location = user.legacy.location; apiUser.location = user.legacy.location;
apiUser.verified = user.legacy.verified; 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_lable = user.affiliates_highlighted_label?.label?.description || '';
}
apiUser.avatar_url = user.legacy.profile_image_url_https; apiUser.avatar_url = user.legacy.profile_image_url_https;
apiUser.joined = user.legacy.created_at; apiUser.joined = user.legacy.created_at;
if (user.legacy_extended_profile?.birthdate) { if (user.legacy_extended_profile?.birthdate) {
@ -36,41 +48,11 @@ const populateUserProperties = async (
return apiUser; return apiUser;
}; };
const writeDataPoint = (
event: FetchEvent,
language: string | undefined,
returnCode: string,
flags?: InputFlags
) => {
console.log('Writing data point...');
if (typeof AnalyticsEngine !== 'undefined') {
const flagString =
Object.keys(flags || {})
// @ts-expect-error - TypeScript doesn't like iterating over the keys, but that's OK
.filter(flag => flags?.[flag])[0] || 'standard';
AnalyticsEngine.writeDataPoint({
blobs: [
event.request.cf?.colo as string /* Datacenter location */,
event.request.cf?.country as string /* Country code */,
event.request.headers.get('user-agent') ??
'' /* User agent (for aggregating bots calling) */,
returnCode /* Return code */,
flagString /* Type of request */,
language ?? '' /* For translate feature */
],
doubles: [0 /* NSFW media = 1, No NSFW Media = 0 */],
indexes: [event.request.headers.get('cf-ray') ?? '' /* CF Ray */]
});
}
};
/* API for Twitter profiles (Users) /* API for Twitter profiles (Users)
Used internally by FixTweet's embed service, or Used internally by FixTweet's embed service, or
available for free using api.fxtwitter.com. */ available for free using api.fxtwitter.com. */
export const userAPI = async ( export const userAPI = async (
username: string, username: string,
language: string | undefined,
event: FetchEvent, event: FetchEvent,
flags?: InputFlags flags?: InputFlags
): Promise<UserAPIResponse> => { ): Promise<UserAPIResponse> => {
@ -80,13 +62,10 @@ export const userAPI = async (
const response: UserAPIResponse = { code: 200, message: 'OK' } as UserAPIResponse; const response: UserAPIResponse = { code: 200, message: 'OK' } as UserAPIResponse;
const apiUser: APIUser = (await populateUserProperties( const apiUser: APIUser = (await populateUserProperties(
userResponse, userResponse,
language
)) as APIUser; )) as APIUser;
/* Finally, staple the User to the response and return it */ /* Finally, staple the User to the response and return it */
response.user = apiUser; response.user = apiUser;
writeDataPoint(event, language, 'OK', flags);
return response; return response;
}; };

View file

@ -144,7 +144,7 @@ const statusRequest = async (
/* Handler for User Profiles */ /* Handler for User Profiles */
const profileRequest = async (request: IRequest, event: FetchEvent, const profileRequest = async (request: IRequest, event: FetchEvent,
flags: InputFlags = {}) => { flags: InputFlags = {}) => {
const { handle, language } = 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') || ''; const userAgent = request.headers.get('User-Agent') || '';
@ -166,21 +166,14 @@ const profileRequest = async (request: IRequest, event: FetchEvent,
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; const username = handle.match(/\w{1,15}/gi)?.[0] as string;
/* Check if request is to api.fxtwitter.com, or the tweet is appended with .json /* Check if request is to api.fxtwitter.com */
Note that unlike TwitFix, FixTweet will never generate embeds for .json, and if (Constants.API_HOST_LIST.includes(url.hostname)) {
in fact we only support .json because it's what people using TwitFix API would
be used to.
*/
if (
url.pathname.match(/\/status(es)?\/\d{2,20}\.(json)/g) !== null ||
Constants.API_HOST_LIST.includes(url.hostname)
) {
console.log('JSON API request'); console.log('JSON API request');
flags.api = true; flags.api = true;
} }
/* Direct media or API access bypasses bot check, returning same response regardless of UA */ /* Direct media or API access bypasses bot check, returning same response regardless of UA */
if (isBotUA || flags.direct || flags.api) { if (isBotUA || flags.api) {
if (isBotUA) { if (isBotUA) {
console.log(`Matched bot UA ${userAgent}`); console.log(`Matched bot UA ${userAgent}`);
} else { } else {
@ -192,7 +185,6 @@ const profileRequest = async (request: IRequest, event: FetchEvent,
username, username,
userAgent, userAgent,
flags, flags,
language,
event event
); );
@ -336,7 +328,7 @@ router.get('/owoembed', async (request: IRequest) => {
We don't currently have custom profile cards yet, We don't currently have custom profile cards yet,
but it's something we might do. Maybe. */ but it's something we might do. Maybe. */
router.get('/:handle', profileRequest); router.get('/:handle', profileRequest);
router.get('/:handle/:language', profileRequest); router.get('/:handle/', profileRequest);
router.get('/i/events/:id', genericTwitterRedirect); router.get('/i/events/:id', genericTwitterRedirect);
router.get('/hashtag/:hashtag', genericTwitterRedirect); router.get('/hashtag/:hashtag', genericTwitterRedirect);

View file

@ -148,7 +148,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: `Due to Twitter API changes, some NSFW Tweets are currently being blocked. We are currently looking into a workaround. 🙏`, ERROR_PRIVATE: `Due to Twitter API changes, some NSFW Tweets are currently being blocked. We are currently looking into a workaround. 🙏`,
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_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://${

View file

@ -202,26 +202,41 @@ type GraphQLUserResponse = {
} }
type GraphQLUser = { type GraphQLUser = {
__typename: "User", __typename: "User";
id: string; // "VXNlcjozNzg0MTMxMzIy", id: string; // "VXNlcjo3ODMyMTQ="
rest_id: string; // "3784131322", rest_id: string; // "783214",
affiliates_highlighted_label: Record<string, unknown>; // {}, 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, is_blue_verified: boolean; // false,
profile_image_shape: 'Circle' | 'Square'; // "Circle", profile_image_shape: 'Circle' | 'Square'|'Hexagon'; // "Circle",
has_nft_avatar: boolean; // false,
legacy: { legacy: {
created_at: string; // "Sat Sep 26 17:20:55 +0000 2015", created_at: string; // "Tue Feb 20 14:35:54 +0000 2007",
default_profile: boolean // false, default_profile: boolean // false,
default_profile_image: boolean // false, default_profile_image: boolean // false,
description: string; // "dangered wolf#3621 https://t.co/eBTS4kksMw", description: string; // "What's happening?!",
entities: { entities: {
description: { description?: {
urls: { urls?: {
display_url: string; // "t.me/dangeredwolf", display_url: string; // "about.twitter.com",
expanded_url: string; // "http://t.me/dangeredwolf", expanded_url: string; // "https://about.twitter.com/",
url: string; // "https://t.co/eBTS4kksMw", url: string; // "https://t.co/DAtOo6uuHk",
indices: [ indices: [
19, 0,
42 23
] ]
}[] }[]
} }
@ -232,29 +247,30 @@ type GraphQLUser = {
friends_count: number; // 2125, friends_count: number; // 2125,
has_custom_timelines: boolean; // true, has_custom_timelines: boolean; // true,
is_translator: boolean; // false, is_translator: boolean; // false,
listed_count: number; // 69, listed_count: number; // 88165,
location: string; // "they/them", location: string; // "everywhere",
media_count: number; // 20839, media_count: number; // 20839,
name: string; // "dangered wolf", name: string; // "Twitter",
normal_followers_count: number; // 4996, normal_followers_count: number; // 65669107,
pinned_tweet_ids_str: string[]; // Array of tweet ids pinned_tweet_ids_str: string[]; // Array of tweet ids, usually one. Empty if no pinned tweet
possibly_sensitive: boolean; // false, possibly_sensitive: boolean; // false,
profile_banner_url: string; // "https://pbs.twimg.com/profile_banners/3784131322/1658599775", profile_banner_url: string; // "https://pbs.twimg.com/profile_banners/783214/1646075315",
profile_image_url_https: string; // "https://pbs.twimg.com/profile_images/1555638673705783299/3gaaetxC_normal.jpg", profile_image_url_https: string; // "https://pbs.twimg.com/profile_images/1488548719062654976/u6qfBBkF_normal.jpg",
profile_interstitial_type: string; // "", profile_interstitial_type: string; // "",
screen_name: string; // "dangeredwolf", screen_name: string; // "Twitter",
statuses_count: number; // 108222, statuses_count: number; // 15047
translator_type: string; // "regular", translator_type: string; // "regular"
verified: boolean; // false, verified: boolean; // false
verified_type: 'Business'|'Government';
withheld_in_countries: [] withheld_in_countries: []
}, },
professional: { professional: {
rest_id: string; // "1508134739420536845", rest_id: string; // "1503055759638159366",
professional_type: string; // "Creator", professional_type: string; // "Creator",
category: [ category: [
{ {
id: number; // 354, id: number; // 354,
name: string // "Fish & Chips Restaurant", name: string // "Community",
icon_name: string; // "IconBriefcaseStroke" icon_name: string; // "IconBriefcaseStroke"
} }
] ]
@ -268,7 +284,7 @@ type GraphQLUser = {
year_visibility: string; // "Public" year_visibility: string; // "Public"
}; };
profile_image_shape: string; // "Circle", profile_image_shape: string; // "Circle",
rest_id: string; // "3784131322", rest_id: string; // "783214",
}, },
is_profile_translatable: false, is_profile_translatable: false,
verification_info: { verification_info: {
@ -282,10 +298,9 @@ type GraphQLUser = {
}; };
to_index: number; // 108 to_index: number; // 108
}[]; }[];
text: string; // "This account is verified because its subscribed to Twitter Blue or is a legacy verified account. Learn more" text?: 'This account is verified because its 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';
} }
} }
}, }
business_account: Record<string, unknown>; // {},
} }

View file

@ -159,7 +159,8 @@ interface APIUser {
location: string; location: string;
url: string; url: string;
protected: boolean; protected: boolean;
verified: boolean; verified: 'legacy' | 'blue'| 'business' | 'government';
verified_lable: string;
followers: number; followers: number;
following: number; following: number;
tweets: number; tweets: number;

View file

@ -19,12 +19,11 @@ export const handleProfile = async (
username: string, username: string,
userAgent?: string, userAgent?: string,
flags?: InputFlags, flags?: InputFlags,
language?: string,
event?: FetchEvent event?: FetchEvent
): Promise<StatusResponse> => { ): Promise<StatusResponse> => {
console.log('Direct?', flags?.direct); console.log('Direct?', flags?.direct);
const api = await userAPI(username, language, event as FetchEvent); const api = await userAPI(username, event as FetchEvent);
const user = api?.user as APIUser; const user = api?.user as APIUser;
/* Catch this request if it's an API response */ /* Catch this request if it's an API response */

View file

@ -315,7 +315,7 @@ test('API fetch poll Tweet', async () => {
test('API fetch user', async () => { test('API fetch user', async () => {
const result = await cacheWrapper( const result = await cacheWrapper(
new Request('https://api.fxtwitter.com/wazbat', { new Request('https://api.fxtwitter.com/twitter', {
method: 'GET', method: 'GET',
headers: botHeaders headers: botHeaders
}) })
@ -328,19 +328,17 @@ test('API fetch user', async () => {
const user = response.user as APIUser; const user = response.user as APIUser;
expect(user).toBeTruthy(); expect(user).toBeTruthy();
expect(user.url).toEqual('https://twitter.com/wazbat'); expect(user.url).toEqual('https://twitter.com/Twitter');
expect(user.id).toEqual('157658332'); expect(user.id).toEqual('783214');
expect(user.name).toEqual('Wazbat'); expect(user.screen_name).toEqual('Twitter');
expect(user.screen_name).toEqual('wazbat'); expect(user.followers).toEqual(expect.any(Number));
expect(user.location).toEqual('Behind you'); expect(user.following).toEqual(expect.any(Number));
expect(user.followers).toBeGreaterThanOrEqual(1_000); // The official twitter account will never be following as many people as it has followers
expect(user.followers).toBeLessThanOrEqual(100_000); expect(user.following).not.toEqual(user.followers);
expect(user.following).toBeGreaterThanOrEqual(10); expect(user.likes).toEqual(expect.any(Number));
expect(user.following).toBeLessThanOrEqual(1_000); expect(user.verified).toEqual('business');
expect(user.likes).toBeGreaterThanOrEqual(10_000); expect(user.joined).toEqual('Tue Feb 20 14:35:54 +0000 2007');
expect(user.verified).toEqual(false); expect(user.birthday.day).toEqual(21);
expect(user.joined).toEqual('Sun Jun 20 13:29:36 +0000 2010'); expect(user.birthday.month).toEqual(3);
expect(user.birthday.day).toEqual(14);
expect(user.birthday.month).toEqual(7);
expect(user.birthday.year).toBeUndefined(); expect(user.birthday.year).toBeUndefined();
}); });