From 2225969824be5455630bcbea3d1f8c1120b58216 Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 21 Mar 2022 15:14:28 +0800 Subject: [PATCH 1/3] feat: replace upload from hook to api --- packages/mask/shared/kv-storage.ts | 5 -- .../injection/NFT/NFTAvatarInInstagram.tsx | 80 +---------------- .../injection/NFT/NFTAvatarSettingDialog.tsx | 85 ++++++++++++++----- .../instagram.com/utils/user.ts | 7 -- packages/web3-providers/src/index.ts | 3 +- .../web3-providers/src/instagram/index.ts | 33 +++++++ packages/web3-providers/src/types.ts | 15 ++++ 7 files changed, 115 insertions(+), 113 deletions(-) create mode 100644 packages/web3-providers/src/instagram/index.ts diff --git a/packages/mask/shared/kv-storage.ts b/packages/mask/shared/kv-storage.ts index 92625c028839..a77cc4915102 100644 --- a/packages/mask/shared/kv-storage.ts +++ b/packages/mask/shared/kv-storage.ts @@ -18,11 +18,6 @@ export const InMemoryStorages = { address: '', tokenId: '', }), - InstagramNFTEvent: createInMemoryKVStorage('InstagramNFTEvent', { - userId: '', - address: '', - tokenId: '', - }), } export const PersistentStorages = { Plugin: createPersistentKVStorage('plugin', {}), diff --git a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarInInstagram.tsx b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarInInstagram.tsx index e677814a0fcc..c7b7958c1722 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarInInstagram.tsx +++ b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarInInstagram.tsx @@ -1,22 +1,17 @@ import { LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-kit' -import { searchInstagramAvatarSelector, searchInstagramAvatarUploadLoadingSelector } from '../../utils/selector' +import { searchInstagramAvatarSelector } from '../../utils/selector' import { createReactRootShadowed, startWatch } from '../../../../utils' import { makeStyles } from '@masknet/theme' -import { useWallet } from '@masknet/plugin-infra' import { useEffect, useMemo, useState } from 'react' import type { AvatarMetaDB } from '../../../../plugins/Avatar/types' import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI' import { useNFTAvatar } from '../../../../plugins/Avatar/hooks' import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants' -import { InMemoryStorages } from '../../../../../shared' -import { clearStorages, getAvatarId } from '../../utils/user' -import { PluginNFTAvatarRPC } from '../../../../plugins/Avatar/messages' +import { getAvatarId } from '../../utils/user' import { useLocation, useWindowSize } from 'react-use' import { NFTBadge } from '../../../../plugins/Avatar/SNSAdaptor/NFTBadge' -import { activatedSocialNetworkUI } from '../../../../social-network' import { max } from 'lodash-unified' import { rainbowBorderKeyFrames } from '../../../../plugins/Avatar/SNSAdaptor/RainbowBox' -import { delay } from '@dimensiondev/kit' export function injectNFTAvatarInInstagram(signal: AbortSignal) { const watcher = new MutationObserverWatcher(searchInstagramAvatarSelector()) @@ -46,7 +41,6 @@ const useStyles = makeStyles()(() => ({ function NFTAvatarInInstagram() { const { classes } = useStyles() - const wallet = useWallet() const [avatar, setAvatar] = useState() const identity = useCurrentVisitingIdentity() @@ -68,76 +62,6 @@ function NFTAvatarInInstagram() { return max([146, Number.parseInt(style.width.replace('px', '') ?? 0, 10)]) }, [windowSize]) - useEffect(() => { - const watcher = new MutationObserverWatcher(searchInstagramAvatarUploadLoadingSelector()) - .addListener('onRemove', async () => { - const storages = InMemoryStorages.InstagramNFTEvent.storage - - if (!wallet) return - - if (storages.address.value && storages.userId.value && storages.tokenId.value) { - try { - await delay(1000) - - const url = `${location.protocol}//${location.host}/${identity.identifier.userId}` - - const response = await fetch(url) - const htmlString = await response.text() - - const html = document.createElement('html') - html.innerHTML = htmlString - - const metaTag = html.querySelector('meta[property="og:image"]') - - if (!metaTag?.content) return - - const avatarInfo = await PluginNFTAvatarRPC.saveNFTAvatar( - wallet.address, - { - userId: storages.userId.value, - tokenId: storages.tokenId.value, - address: storages.address.value, - avatarId: getAvatarId(metaTag.content), - } as AvatarMetaDB, - identity.identifier.network, - RSS3_KEY_SNS.INSTAGRAM, - ) - - if (!avatarInfo) { - clearStorages() - setAvatar(undefined) - window.alert('Sorry, failed to save NFT Avatar. Please set again.') - return - } - - setAvatar(avatarInfo) - clearStorages() - await PluginNFTAvatarRPC.clearCache( - identity.identifier.userId, - activatedSocialNetworkUI.networkIdentifier, - RSS3_KEY_SNS.INSTAGRAM, - ) - // If the avatar is set successfully, reload the page - window.location.reload() - } catch (error: any) { - clearStorages() - setAvatar(undefined) - window.alert(error.message) - return - } - } - }) - .startWatch({ - childList: true, - subtree: true, - attributes: true, - }) - - return () => { - watcher.stopWatch() - } - }, [identity, wallet]) - useEffect(() => { if (!showAvatar) return diff --git a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx index 40082443c1ff..f95d72a28753 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx +++ b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx @@ -1,17 +1,21 @@ -import { searchInstagramAvatarOpenFilesSelector } from '../../utils/selector' import { MaskMessages, useI18N } from '../../../../utils' import { useCallback, useState } from 'react' import { useCurrentVisitingIdentity } from '../../../../components/DataSource/useActivatedUI' import { toPNG } from '../../../../plugins/Avatar/utils' -import { InMemoryStorages } from '../../../../../shared' import { useMount } from 'react-use' -import { clearStorages } from '../../utils/user' +import { getAvatarId } from '../../utils/user' import { InjectedDialog } from '../../../../components/shared/InjectedDialog' import { DialogContent } from '@mui/material' import { NFTAvatar } from '../../../../plugins/Avatar/SNSAdaptor/NFTAvatar' import { DialogStackingProvider, makeStyles } from '@masknet/theme' -import { hookInputUploadOnce } from '@masknet/injected-script' import type { ERC721TokenDetailed } from '@masknet/web3-shared-evm' +import { Instagram } from '@masknet/web3-providers' +import { useWallet } from '@masknet/plugin-infra' +import { PluginNFTAvatarRPC } from '../../../../plugins/Avatar/messages' +import type { AvatarMetaDB } from '../../../../plugins/Avatar/types' +import { RSS3_KEY_SNS } from '../../../../plugins/Avatar/constants' +import { activatedSocialNetworkUI } from '../../../../social-network' +import { delay } from '@dimensiondev/kit' const useStyles = makeStyles()(() => ({ root: { @@ -20,38 +24,75 @@ const useStyles = makeStyles()(() => ({ }, })) -async function changeImageToActiveElements(image: File | Blob): Promise { - const imageBuffer = await image.arrayBuffer() - hookInputUploadOnce('image/png', 'avatar.png', new Uint8Array(imageBuffer)) - searchInstagramAvatarOpenFilesSelector().evaluate()?.click() -} - export function NFTAvatarSettingDialog() { const { t } = useI18N() const [open, setOpen] = useState(false) const { classes } = useStyles() - + const wallet = useWallet() const identity = useCurrentVisitingIdentity() const onChange = useCallback( async (token: ERC721TokenDetailed) => { - if (!token.info.imageURL) return - const image = await toPNG(token.info.imageURL) - if (!image) return - await changeImageToActiveElements(image) - - InMemoryStorages.InstagramNFTEvent?.storage.userId.setValue(identity.identifier.userId) - InMemoryStorages.InstagramNFTEvent?.storage.address.setValue(token.contractDetailed.address) - InMemoryStorages.InstagramNFTEvent?.storage.tokenId.setValue(token.tokenId) - setOpen(false) + try { + if (!token.info.imageURL) return + const image = await toPNG(token.info.imageURL) + if (!image || !wallet) return + + await Instagram.uploadUserAvatar(image, identity.identifier.userId) + + await delay(1000) + + const url = `${location.protocol}//${location.host}/${identity.identifier.userId}` + + const response = await fetch(url) + const htmlString = await response.text() + + const html = document.createElement('html') + html.innerHTML = htmlString + + const metaTag = html.querySelector('meta[property="og:image"]') + + if (!metaTag?.content) return + + const avatarInfo = await PluginNFTAvatarRPC.saveNFTAvatar( + wallet.address, + { + userId: identity.identifier.userId, + tokenId: token.tokenId, + address: token.contractDetailed.address, + avatarId: getAvatarId(metaTag.content), + } as AvatarMetaDB, + identity.identifier.network, + RSS3_KEY_SNS.INSTAGRAM, + ) + + if (!avatarInfo) { + window.alert('Sorry, failed to save NFT Avatar. Please set again.') + setOpen(false) + return + } + + await PluginNFTAvatarRPC.clearCache( + identity.identifier.userId, + activatedSocialNetworkUI.networkIdentifier, + RSS3_KEY_SNS.INSTAGRAM, + ) + + // If the avatar is set successfully, reload the page + window.location.reload() + + setOpen(false) + } catch (error: any) { + window.alert(error.message) + return + } }, - [identity], + [identity, wallet], ) const onClose = useCallback(() => setOpen(false), []) useMount(() => { - clearStorages() return MaskMessages.events.nftAvatarSettingDialogUpdated.on((data) => setOpen(data.open)) }) diff --git a/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts b/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts index 01c32de19892..d579a81de14e 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts +++ b/packages/mask/src/social-network-adaptor/instagram.com/utils/user.ts @@ -7,7 +7,6 @@ import { searchUserIdInEditPageSelector, searchUserIdSelector, } from './selector' -import { InMemoryStorages } from '../../../../shared' export function getBioDescription() { const bio = bioDescriptionSelector().evaluate() @@ -40,12 +39,6 @@ export const getAvatar = () => { return imageURL.trim() } -export const clearStorages = () => { - InMemoryStorages.InstagramNFTEvent.storage.userId.setValue('') - InMemoryStorages.InstagramNFTEvent.storage.address.setValue('') - InMemoryStorages.InstagramNFTEvent.storage.tokenId.setValue('') -} - const INSTAGRAM_AVATAR_ID_MATCH = /(\w+).(?:png|jpg|gif|bmp)/ export const getAvatarId = (avatarURL: string) => { diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index 113f736adb2e..d3989c5950fc 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -9,6 +9,7 @@ import { KeyValueAPI } from './kv' import { TwitterAPI } from './twitter' import { TokenListAPI } from './token-list' import { TokenPriceAPI } from './token-price' +import { InstagramAPI } from './instagram' export * from './types' export * from './hooks' @@ -24,7 +25,7 @@ export const Explorer = new NativeExplorerAPI() export const RSS3 = new RSS3API() export const KeyValue = new KeyValueAPI() export const Twitter = new TwitterAPI() - +export const Instagram = new InstagramAPI() export const TokenList = new TokenListAPI() export const TokenPrice = new TokenPriceAPI() diff --git a/packages/web3-providers/src/instagram/index.ts b/packages/web3-providers/src/instagram/index.ts new file mode 100644 index 000000000000..fde2cd0947e9 --- /dev/null +++ b/packages/web3-providers/src/instagram/index.ts @@ -0,0 +1,33 @@ +import type { InstagramBaseAPI } from '../types' + +const UPLOAD_AVATAR_URL = 'https://www.instagram.com/accounts/web_change_profile_picture/' + +function getCSRFToken() { + const csrfToken = document.cookie.split('; ').find((x) => x.includes('csrftoken')) + if (!csrfToken) return '' + const [, value] = csrfToken.split('=') + return value +} + +export class InstagramAPI implements InstagramBaseAPI.Provider { + async uploadUserAvatar( + image: File | Blob, + userId: string, + ): Promise<{ changed_profile: boolean; profile_pic_url_hd: string }> { + const formData = new FormData() + const csrfToken = getCSRFToken() + formData.append('profile_pic', image) + + const response = await fetch(UPLOAD_AVATAR_URL, { + method: 'POST', + mode: 'cors', + headers: { + 'x-csrfToken': csrfToken, + referer: `https://www.instagram.com/${userId}/`, + }, + body: formData, + }) + + return response.json() + } +} diff --git a/packages/web3-providers/src/types.ts b/packages/web3-providers/src/types.ts index 552fbece0567..5e4a0def0320 100644 --- a/packages/web3-providers/src/types.ts +++ b/packages/web3-providers/src/types.ts @@ -394,6 +394,21 @@ export namespace TwitterBaseAPI { } } +export namespace InstagramBaseAPI { + export interface Provider { + uploadUserAvatar: ( + image: File | Blob, + userId: string, + ) => Promise< + | { + changed_profile: boolean + profile_pic_url_hd: string + } + | undefined + > + } +} + export namespace TokenListBaseAPI { export interface Token { address: string From e9475acee1d4cfc73fadc47fd8c247f6ccfeb3aa Mon Sep 17 00:00:00 2001 From: nuanyang233 <528944303@qq.com> Date: Mon, 21 Mar 2022 16:22:29 +0800 Subject: [PATCH 2/3] fix: remove useless selector --- cspell.json | 1 + .../instagram.com/utils/selector.ts | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/cspell.json b/cspell.json index ab6b0b7bf5fa..4276cc37b457 100644 --- a/cspell.json +++ b/cspell.json @@ -289,6 +289,7 @@ "cotm", "cryptoart", "cryptoartai", + "csrftoken", "cusd", "defi", "devnet", diff --git a/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts index f6c608b7c3b2..c90ade6b61c7 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts +++ b/packages/mask/src/social-network-adaptor/instagram.com/utils/selector.ts @@ -46,9 +46,6 @@ export const searchProfileTabOtherArticlePageSelector = () => export const searchInstagramAvatarListSelector = () => querySelector('[role="dialog"] .piCib > div > form').closest(1).querySelector('button') -export const searchInstagramAvatarOpenFilesSelector = () => - querySelector('[id="react-root"] button > img').closest(4).querySelector('form') - export const searchInstagramSaveAvatarButtonSelector = () => querySelector('section > div > header > div > div:last-child > button') @@ -60,11 +57,6 @@ export const searchInstagramProfileAvatarButtonSelector = () => export const searchInstagramAvatarSettingDialog = () => querySelector('[id="react-root"]') -export const searchInstagramAvatarUploadLoadingSelector = () => - querySelector('[id="react-root"] button') - .closest(1) - .querySelector('div[data-visualcompletion="loading-state"]') - export const searchInstagramProfileSettingButtonSelector = () => querySelector('[id="react-root"] header button').closest(4).querySelector('section > div > div') From cb67c8292f8aad82808e12f88df93d8e3c8aed72 Mon Sep 17 00:00:00 2001 From: nuanyang233 Date: Mon, 21 Mar 2022 17:05:30 +0800 Subject: [PATCH 3/3] chore: reply review --- .../injection/NFT/NFTAvatarSettingDialog.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx index f95d72a28753..3442a7af4232 100644 --- a/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx +++ b/packages/mask/src/social-network-adaptor/instagram.com/injection/NFT/NFTAvatarSettingDialog.tsx @@ -82,9 +82,11 @@ export function NFTAvatarSettingDialog() { window.location.reload() setOpen(false) - } catch (error: any) { - window.alert(error.message) - return + } catch (error) { + if (error instanceof Error) { + window.alert(error.message) + return + } } }, [identity, wallet],