Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
"cotm",
"cryptoart",
"cryptoartai",
"csrftoken",
"cusd",
"defi",
"devnet",
Expand Down
5 changes: 0 additions & 5 deletions packages/mask/shared/kv-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ export const InMemoryStorages = {
address: '',
tokenId: '',
}),
InstagramNFTEvent: createInMemoryKVStorage('InstagramNFTEvent', {
userId: '',
address: '',
tokenId: '',
}),
}
export const PersistentStorages = {
Plugin: createPersistentKVStorage('plugin', {}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
Expand Down Expand Up @@ -46,7 +41,6 @@ const useStyles = makeStyles()(() => ({

function NFTAvatarInInstagram() {
const { classes } = useStyles()
const wallet = useWallet()
const [avatar, setAvatar] = useState<AvatarMetaDB>()

const identity = useCurrentVisitingIdentity()
Expand All @@ -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<HTMLMetaElement>('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

Expand Down
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -20,38 +24,77 @@ const useStyles = makeStyles()(() => ({
},
}))

async function changeImageToActiveElements(image: File | Blob): Promise<void> {
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}`
Comment thread
nuanyang233 marked this conversation as resolved.

const response = await fetch(url)
const htmlString = await response.text()

const html = document.createElement('html')
html.innerHTML = htmlString

const metaTag = html.querySelector<HTMLMetaElement>('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) {
if (error instanceof Error) {
window.alert(error.message)
return
}
}
},
[identity],
[identity, wallet],
)

const onClose = useCallback(() => setOpen(false), [])

useMount(() => {
clearStorages()
return MaskMessages.events.nftAvatarSettingDialogUpdated.on((data) => setOpen(data.open))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLFormElement>('form')

export const searchInstagramSaveAvatarButtonSelector = () =>
querySelector<HTMLButtonElement>('section > div > header > div > div:last-child > button')

Expand All @@ -60,11 +57,6 @@ export const searchInstagramProfileAvatarButtonSelector = () =>

export const searchInstagramAvatarSettingDialog = () => querySelector<E>('[id="react-root"]')

export const searchInstagramAvatarUploadLoadingSelector = () =>
querySelector('[id="react-root"] button')
.closest(1)
.querySelector<HTMLDivElement>('div[data-visualcompletion="loading-state"]')

export const searchInstagramProfileSettingButtonSelector = () =>
querySelector('[id="react-root"] header button').closest(4).querySelector('section > div > div')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
searchUserIdInEditPageSelector,
searchUserIdSelector,
} from './selector'
import { InMemoryStorages } from '../../../../shared'

export function getBioDescription() {
const bio = bioDescriptionSelector().evaluate()
Expand Down Expand Up @@ -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) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/web3-providers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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()

Expand Down
33 changes: 33 additions & 0 deletions packages/web3-providers/src/instagram/index.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}
15 changes: 15 additions & 0 deletions packages/web3-providers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down