Skip to content

Commit

Permalink
fix: twitter identity 429 (#10837)
Browse files Browse the repository at this point in the history
* refactor: add TwitterBaseAPI.User

* chore: add getUserByScreenNameShow

* refactor: code style

* refactor: cache duration

* refactor: stale

* refactor: adjust sequence
  • Loading branch information
guanbinrui committed Sep 24, 2023
1 parent 2de662b commit 77c920f
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 107 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Twitter } from '@masknet/web3-providers'
import { useAsync } from 'react-use'
import { Twitter } from '@masknet/web3-providers'
import { RSS3_KEY_SITE, NFTAvatarMiniClip, NFTBadgeTimeline } from '@masknet/plugin-avatar'

interface Props {
Expand All @@ -16,15 +16,13 @@ export function AvatarDecoration({ clipPathId, userId, className, size }: Props)

if (!userId || !user) return null

const avatarId = Twitter.getAvatarId(user.legacy?.profile_image_url_https)

return user.has_nft_avatar ? (
<NFTAvatarMiniClip className={className} id={clipPathId} size={size} screenName={userId} />
) : (
<NFTBadgeTimeline
classes={{ root: className }}
userId={userId}
avatarId={avatarId}
avatarId={Twitter.getAvatarId(user.avatarURL)}
height={size}
width={size}
siteKey={RSS3_KEY_SITE.TWITTER}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,20 @@ function resolveCurrentVisitingIdentityInner(
const update = async (twitterId: string) => {
const user = await Twitter.getUserByScreenName(twitterId)
if (process.env.NODE_ENV === 'development') {
console.assert(user?.legacy, `Can't get get user by screen name ${twitterId}`)
console.assert(user, `Can't get get user by screen name ${twitterId}`)
}
if (!user?.legacy) return
if (!user) return

const nickname = user.legacy.name
const handle = user.legacy.screen_name
const handle = user.screenName
const ownerHandle = ownerRef.value.identifier?.userId
const isOwner = !!(ownerHandle && handle.toLowerCase() === ownerHandle.toLowerCase())
const avatar = user.legacy.profile_image_url_https.replace(/_normal(\.\w+)$/, '_400x400$1')
const bio = user.legacy.description
const homepage = user.legacy.entities.url?.urls[0]?.expanded_url ?? ''
const avatar = user.avatarURL
const bio = user.bio
const homepage = user.homepage

ref.value = {
identifier: ProfileIdentifier.of(twitterBase.networkIdentifier, handle).unwrapOr(undefined),
nickname,
nickname: user.nickname,
avatar,
bio,
homepage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { usePersonasFromDB } from '../../../../components/DataSource/usePersonas
import Services from '#services'
import { attachReactTreeWithContainer } from '../../../../utils/shadow-root/renderInShadowRoot.js'
import { ButtonStyle } from '../../constant.js'
import { searchProfileAvatarSelector, searchProfileSaveSelector } from '../../utils/selector.js'
import { startWatch } from '../../../../utils/startWatch.js'
import { searchProfileAvatarSelector, searchProfileSaveSelector } from '../../utils/selector.js'

export function injectOpenNFTAvatarEditProfileButtonAtEditProfileDialog(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(searchProfileAvatarSelector())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useEffect, useRef, useState } from 'react'
import { useAsync } from 'react-use'
import { Fade } from '@mui/material'
import { CrossIsolationMessages, ProfileIdentifier, type SocialIdentity } from '@masknet/shared-base'
import { LoadingBase, ShadowRootPopper, makeStyles } from '@masknet/theme'
import { AnchorProvider, queryClient } from '@masknet/shared-base-ui'
import { Twitter } from '@masknet/web3-providers'
import { useSocialIdentity } from '../../../../components/DataSource/useActivatedUI.js'
import { ProfileCard } from '../../../../components/InjectedComponents/ProfileCard/index.js'
import { attachReactTreeWithoutContainer } from '../../../../utils/index.js'
import { twitterBase } from '../../base.js'
import { CARD_HEIGHT, CARD_WIDTH } from './constants.js'
import { useControlProfileCard } from './useControlProfileCard.js'
import { Fade } from '@mui/material'
import { AnchorProvider, queryClient } from '@masknet/shared-base-ui'

export function injectProfileCardHolder(signal: AbortSignal) {
attachReactTreeWithoutContainer('profile-card', <ProfileCardHolder />, signal)
Expand Down Expand Up @@ -59,16 +59,14 @@ function ProfileCardHolder() {
queryKey: ['twitter', 'profile', twitterId],
queryFn: () => Twitter.getUserByScreenName(twitterId),
})
if (!user?.legacy) return null

const handle = user.legacy.screen_name
if (!user) return null

return {
identifier: ProfileIdentifier.of(twitterBase.networkIdentifier, handle).unwrapOr(undefined),
nickname: user.legacy.name,
avatar: user.legacy.profile_image_url_https.replace(/_normal(\.\w+)$/, '_400x400$1'),
bio: user.legacy.description,
homepage: user.legacy.entities.url?.urls[0]?.expanded_url ?? '',
identifier: ProfileIdentifier.of(twitterBase.networkIdentifier, user.screenName).unwrapOr(undefined),
nickname: user.nickname,
avatar: user.avatarURL,
bio: user.bio,
homepage: user.homepage,
}
}, [twitterId])

Expand Down
32 changes: 12 additions & 20 deletions packages/mask/src/site-adaptors/twitter.com/utils/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ export const getNickname = () => {

export const getTwitterId = () => {
const ele = searchAvatarSelector().evaluate()?.closest('a') || searchNFTAvatarSelector().evaluate()?.closest('a')
if (ele) {
const link = ele.getAttribute('href')
if (link) {
const [, userId] = link.match(/^\/(\w+)\/(photo|nft)$/) ?? []
return userId
}
}
if (!ele) return ''

return ''
const link = ele.getAttribute('href')
if (!link) return ''

const [, userId] = link.match(/^\/(\w+)\/(photo|nft)$/) ?? []
return userId
}

export const getBio = () => {
Expand All @@ -67,20 +65,14 @@ export const getAvatar = () => {

export async function getUserIdentity(twitterId: string): Promise<SocialIdentity | undefined> {
const user = await Twitter.getUserByScreenName(twitterId)
if (!user?.legacy) return

const nickname = user.legacy.name
const handle = user.legacy.screen_name
const avatar = user.legacy.profile_image_url_https.replace(/_normal(\.\w+)$/, '_400x400$1')
const bio = user.legacy.description
const homepage = user.legacy.entities.url?.urls[0]?.expanded_url ?? ''
if (!user) return

return {
identifier: ProfileIdentifier.of(twitterBase.networkIdentifier, handle).unwrapOr(undefined),
nickname,
avatar,
bio,
homepage,
identifier: ProfileIdentifier.of(twitterBase.networkIdentifier, user.screenName).unwrapOr(undefined),
nickname: user.nickname,
avatar: user.avatarURL,
bio: user.bio,
homepage: user.homepage,
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/mask/src/utils/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function canAccessAsContent(url: string) {
const target = new URL(url, location.href)
if (
location.origin.endsWith('twitter.com') &&
['https://abs.twimg.com', 'https://upload.twitter.com'].includes(target.origin)
['https://abs.twimg.com', 'https://upload.twitter.com', 'https://api.twitter.com'].includes(target.origin)
)
return true

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { memo, useState, useCallback, useMemo, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Icons } from '@masknet/icons'
import { PlatformAvatar, WalletSettingCard } from '@masknet/shared'
import { useI18N } from '../../locales/index.js'
import { type BindingProof, EMPTY_LIST } from '@masknet/shared-base'
import { makeStyles } from '@masknet/theme'
import { Twitter } from '@masknet/web3-providers'
Expand All @@ -15,9 +16,8 @@ import {
Button,
alpha,
} from '@mui/material'
import { useQuery } from '@tanstack/react-query'
import { memo, useState, useCallback, useMemo, useRef } from 'react'
import { resolveNextIDPlatformWalletName } from '@masknet/web3-shared-base'
import { useI18N } from '../../locales/index.js'

const useStyles = makeStyles()((theme) => ({
card: {
Expand Down Expand Up @@ -131,10 +131,10 @@ export const ProfileCard = memo(function ProfileCard({
queryKey: ['twitter', 'profile', profile.identity],
queryFn: () => Twitter.getUserByScreenName(profile.identity),
})
const nickname = user?.legacy?.name || profile.name || profile.identity
const nickname = user?.nickname || profile.name || profile.identity
// Identities of Twitter proof get lowered case. Prefer handle from Twitter API.
const handle = user?.legacy?.screen_name || profile.identity
const avatarUrl = user?.legacy?.profile_image_url_https || avatar
const handle = user?.screenName || profile.identity
const avatarUrl = user?.avatarURL || avatar
const handleSwitch = useCallback(
(address: string) => {
onToggle?.(profile.identity, address)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import urlcat from 'urlcat'
import { isNull } from 'lodash-es'
import { attemptTimes } from '@masknet/web3-shared-base'
import { getHeaders } from './getTokens.js'
import { fetchGlobal } from '../../helpers/fetchGlobal.js'
import { Expiration } from '../../helpers/fetchSquashed.js'
Expand Down Expand Up @@ -42,17 +40,32 @@ async function createRequest(screenName: string) {
})
}

export async function getUserViaWebAPI(screenName: string): Promise<TwitterBaseAPI.User | null> {
function createUser(response: TwitterBaseAPI.UserByScreenNameResponse) {
const result = response.data.user.result
return {
verified: result.legacy?.verified ?? false,
has_nft_avatar: result.has_nft_avatar ?? false,
userId: result.rest_id,
nickname: result.legacy?.name ?? '',
screenName: result.legacy?.screen_name ?? '', // handle
avatarURL: result.legacy?.profile_image_url_https.replace(/_normal(\.\w+)$/, '_400x400$1'),
bio: result.legacy?.description,
location: result.legacy?.location,
homepage: result.legacy?.entities.url?.urls[0]?.expanded_url,
}
}

export async function getUserByScreenName(screenName: string): Promise<TwitterBaseAPI.User | null> {
const request = await createRequest(screenName)
if (!request) return null

const response = await fetchGlobal(request, undefined, {
cacheDuration: Duration.ONE_MINUTE,
cacheDuration: Duration.ONE_DAY,
squashExpiration: Expiration.ONE_SECOND,
})
if (response.ok) {
const json: TwitterBaseAPI.UserByScreenNameResponse = await response.json()
return json.data.user.result
return createUser(json)
}

const patchingFeatures: string[] = []
Expand All @@ -72,17 +85,13 @@ export async function getUserViaWebAPI(screenName: string): Promise<TwitterBaseA
return null
}

export const getUserViaWebTimesAPI = (screenName: string) => {
return attemptTimes(() => getUserViaWebAPI(screenName), null, isNull)
}

export async function staleUserViaWebAPI(screenName: string): Promise<TwitterBaseAPI.User | null> {
export async function staleUserByScreenName(screenName: string): Promise<TwitterBaseAPI.User | null> {
const request = await createRequest(screenName)
if (!request) return null

const response = await staleCached(request)
if (!response?.ok) return null

const json: TwitterBaseAPI.UserByScreenNameResponse = await response.json()
return json.data.user.result
return createUser(json)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import urlcat from 'urlcat'
import { getHeaders } from './getTokens.js'
import { Duration, staleCached } from '../../helpers/fetchCached.js'
import { Expiration } from '../../helpers/fetchSquashed.js'
import { fetchCachedJSON } from '../../helpers/fetchJSON.js'
import type { TwitterBaseAPI } from '../../entry-types.js'

function createUser(response: TwitterBaseAPI.ShowResult): TwitterBaseAPI.User {
return {
verified: response.verified,
has_nft_avatar: false,
userId: response.id_str,
nickname: response.name,
screenName: response.screen_name,
avatarURL: response.profile_image_url_https.replace(/_normal(\.\w+)$/, '_400x400$1'),
bio: response.description,
location: response.location,
homepage: response.entities?.url?.urls[0]?.expanded_url,
}
}

async function createRequest(screenName: string) {
const url = urlcat('https://api.twitter.com/1.1/users/show.json', {
screen_name: screenName,
})
return new Request(url, {
headers: await getHeaders(),
credentials: 'include',
})
}

export async function getUserByScreenNameShow(screenName: string): Promise<TwitterBaseAPI.User | null> {
const request = await createRequest(screenName)
const response = await fetchCachedJSON<TwitterBaseAPI.ShowResult>(request, undefined, {
cacheDuration: Duration.ONE_DAY,
squashExpiration: Expiration.ONE_SECOND,
})
return createUser(response)
}

export async function staleUserByScreenNameShow(screenName: string): Promise<TwitterBaseAPI.User | null> {
const request = await createRequest(screenName)
if (!request) return null

const response = await staleCached(request)
if (!response?.ok) return null

const json: TwitterBaseAPI.ShowResult = await response.json()
return createUser(json)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import urlcat from 'urlcat'
import { getHeaders } from './getTokens.js'
import { fetchCachedJSON } from '../../helpers/fetchJSON.js'
import { Duration } from '../../helpers/fetchCached.js'
import { Expiration } from '../../helpers/fetchSquashed.js'
import type { TwitterBaseAPI } from '../../entry-types.js'
import { Duration } from '../../entry-helpers.js'

export async function getUserNFTContainer(screenName: string) {
return fetchCachedJSON<{
Expand All @@ -25,6 +26,7 @@ export async function getUserNFTContainer(screenName: string) {
},
{
cacheDuration: Duration.ONE_DAY,
squashExpiration: Expiration.ONE_SECOND,
},
)
}

0 comments on commit 77c920f

Please sign in to comment.