Skip to content

Commit cae68bb

Browse files
committed
fixup! feat: mf-6109 detect scam addresses and links in post
1 parent 71f0a13 commit cae68bb

File tree

19 files changed

+213
-52
lines changed

19 files changed

+213
-52
lines changed

packages/icons/icon-generated-as-jsx.js

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/icons/icon-generated-as-url.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/icons/plugins/GoPlus.svg

+9
Loading

packages/mask/background/services/helper/short-link-resolver.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ async function resolver(u: string): Promise<string | null> {
1717
return url ?? null
1818
}
1919
/** Resolve a https://t.co/ link to it's real address. */
20-
export const resolveTCOLink = memoizePromise(memoize, resolver, (x) => x)
20+
export const resolveTCOLink: (u: string) => Promise<string | null> = memoizePromise(memoize, resolver, (x) => x)

packages/mask/content-script/site-adaptors/twitter.com/injection/PostReplacer.tsx

+31-13
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,42 @@ function resolveLangNode(node: HTMLElement) {
88
)
99
}
1010

11-
export function injectPostReplacerAtTwitter(signal: AbortSignal, current: PostInfo) {
11+
// There are different from what ScamWarning uses, they are not global
12+
const EVM_ADDRESS = /(^|\s)(0x[\dA-Fa-f]{40})/
13+
const SOLANA_ADDRESS = /(^|\s)([1-9A-HJ-NP-Za-km-z]{32,44})/
14+
const TORN_ADDRESS = /(^|\s)(T[1-9A-Za-z]{33})/
15+
16+
function detectPotentialScam(postInfo: PostInfo) {
17+
const textContent = postInfo.rootNode?.textContent?.trim()
18+
if (!textContent) return false
19+
const hasAddresses =
20+
EVM_ADDRESS.test(textContent) || SOLANA_ADDRESS.test(textContent) || TORN_ADDRESS.test(textContent)
21+
const hasLinks = postInfo.mentionedLinks.getCurrentValue().length > 0
22+
return hasAddresses || hasLinks
23+
}
24+
25+
export async function injectPostReplacerAtTwitter(signal: AbortSignal, current: PostInfo) {
1226
const rootNode = current.rootNode
27+
const hasPotentialScam = detectPotentialScam(current)
1328
if (!rootNode) return
1429
const isPromotionPost = !!rootNode.querySelector('svg path[d$="996V8h7v7z"]')
15-
const isCollapsedPost = !!rootNode.querySelector('[data-testid="tweet-text-show-more-link"]')
16-
if (isPromotionPost || isCollapsedPost) return
30+
if (isPromotionPost) return
31+
if (!hasPotentialScam) {
32+
const isCollapsedPost = !!rootNode.querySelector('[data-testid="tweet-text-show-more-link"]')
33+
if (isCollapsedPost) return
1734

18-
const hasVideo = !!rootNode.closest('[data-testid="tweet"]')?.querySelector('video')
19-
if (hasVideo) return
20-
const hasEmbedImage = !!rootNode.querySelector('[data-testid="tweetText"] [data-testid="tweetPhoto"]')
21-
if (hasEmbedImage) return
35+
const hasVideo = !!rootNode.closest('[data-testid="tweet"]')?.querySelector('video')
36+
if (hasVideo) return
37+
const hasEmbedImage = !!rootNode.querySelector('[data-testid="tweetText"] [data-testid="tweetPhoto"]')
38+
if (hasEmbedImage) return
2239

23-
const tags = Array.from(
24-
rootNode.querySelectorAll<HTMLAnchorElement>(
25-
['a[role="link"][href*="cashtag_click"]', 'a[role="link"][href*="hashtag_click"]'].join(','),
26-
) ?? [],
27-
)
28-
if (!tags.map((x) => x.textContent).some((x) => x && /^[#$]\w+$/i.test(x) && x.length <= 9)) return
40+
const tags = Array.from(
41+
rootNode.querySelectorAll<HTMLAnchorElement>(
42+
['a[role="link"][href*="cashtag_click"]', 'a[role="link"][href*="hashtag_click"]'].join(','),
43+
) ?? [],
44+
)
45+
if (!tags.map((x) => x.textContent).some((x) => x && /^[#$]\w+$/i.test(x) && x.length <= 9)) return
46+
}
2947

3048
return injectPostReplacer({
3149
zipPost(node) {

packages/mask/shared-ui/initUIContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ export function setupUIContext() {
4949
hasHostPermission: Services.Helper.hasHostPermission,
5050
requestHostPermission: (origins: readonly string[]) =>
5151
Services.Helper.requestExtensionPermissionFromContentScript({ origins: [...origins] }),
52+
resolveTCOLink: Services.Helper.resolveTCOLink,
5253
})
5354
}

packages/plugin-infra/src/dom/context.ts

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface __UIContext__ {
5555
setPluginMinimalModeEnabled: ((id: string, enabled: boolean) => Promise<void>) | undefined
5656
hasHostPermission: ((origins: readonly string[]) => Promise<boolean>) | undefined
5757
requestHostPermission: ((origins: readonly string[]) => Promise<boolean>) | undefined
58+
resolveTCOLink: (url: string) => Promise<string | null>
5859
}
5960
export let allPersonas: __UIContext__['allPersonas']
6061
export let currentPersona: __UIContext__['currentPersona']
@@ -72,6 +73,7 @@ export let attachProfile: __UIContext__['attachProfile']
7273
export let setPluginMinimalModeEnabled: __UIContext__['setPluginMinimalModeEnabled']
7374
export let hasHostPermission: __UIContext__['hasHostPermission']
7475
export let requestHostPermission: __UIContext__['requestHostPermission']
76+
export let resolveTCOLink: __UIContext__['resolveTCOLink']
7577

7678
export function __setUIContext__(value: __UIContext__) {
7779
;({
@@ -91,5 +93,6 @@ export function __setUIContext__(value: __UIContext__) {
9193
setPluginMinimalModeEnabled,
9294
hasHostPermission,
9395
requestHostPermission,
96+
resolveTCOLink,
9497
} = value)
9598
}

packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { Icons } from '@masknet/icons'
22
import type { Plugin } from '@masknet/plugin-infra'
3+
import { resolveTCOLink } from '@masknet/plugin-infra/dom/context'
34
import { makeStyles, ShadowRootPopper } from '@masknet/theme'
45
import { Link } from '@mui/material'
56
import { useQuery } from '@tanstack/react-query'
67
import { memo } from 'react'
8+
import { PluginScamRPC } from '../../messages.js'
79
import { usePopoverControl } from './usePopoverControl.js'
810
import { WarningCard } from './WarningCard.js'
11+
import { SecurityProvider } from '../../constants.js'
12+
import { GoPlusLabs } from '@masknet/web3-providers'
913

1014
const useStyles = makeStyles()({
1115
link: {
@@ -24,21 +28,38 @@ const useStyles = makeStyles()({
2428
},
2529
})
2630

31+
function isTCO(url: string | null) {
32+
if (!url) return false
33+
return url.startsWith('https://t.co/')
34+
}
35+
2736
export const LinkModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['LinkModifier']>>(function ModifyLink({
2837
fallback,
2938
...props
3039
}) {
3140
const { classes } = useStyles()
32-
const { data: isScam = true } = useQuery({
41+
const { data } = useQuery({
3342
queryKey: ['scam-warning', 'check-link', props.href],
34-
queryFn: () => {
35-
// return PluginScamRPC.checkUrl(props.href) || true
36-
return true
43+
queryFn: async () => {
44+
const resolvedLink = isTCO(props.href) ? await resolveTCOLink(props.href) : props.href
45+
if (!resolvedLink) return { isScam: false }
46+
const result = await GoPlusLabs.checkIsPhishingSite(resolvedLink)
47+
if (result)
48+
return {
49+
isScam: result,
50+
provider: SecurityProvider.GoPlus,
51+
resolvedLink,
52+
}
53+
return {
54+
isScam: await PluginScamRPC.checkUrl(resolvedLink),
55+
provider: SecurityProvider.ScamSniffer,
56+
resolvedLink,
57+
}
3758
},
3859
})
3960
const { open, anchorEl, iconRef, onMouseEnter, onMouseLeave } = usePopoverControl()
4061

41-
if (!isScam) return fallback
62+
if (!data?.isScam) return fallback
4263

4364
return (
4465
<span className={classes.link}>
@@ -51,7 +72,8 @@ export const LinkModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['LinkModi
5172
/>
5273
<ShadowRootPopper open={open} anchorEl={anchorEl}>
5374
<WarningCard
54-
link={props.href}
75+
link={data.resolvedLink || props.href}
76+
securityProvider={data.provider!}
5577
onMouseEnter={onMouseEnter}
5678
onMouseLeave={onMouseLeave}
5779
onClick={(e) => e.stopPropagation()}

packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Icons } from '@masknet/icons'
22
import type { Plugin } from '@masknet/plugin-infra'
33
import { makeStyles, ShadowRootPopper } from '@masknet/theme'
4+
import { GoPlusLabs } from '@masknet/web3-providers'
45
import { isValidAddress } from '@masknet/web3-shared-evm'
56
import { isValidAddress as isSolAddress } from '@masknet/web3-shared-solana'
67
import { useQuery } from '@tanstack/react-query'
78
import { Fragment, memo, useMemo } from 'react'
8-
import { EVM_ADDRESS, SOLANA_ADDRESS } from '../../constants.js'
9+
import { EVM_ADDRESS, SecurityProvider, SOLANA_ADDRESS, TRON_ADDRESS } from '../../constants.js'
910
import { PluginScamRPC } from '../../messages.js'
11+
import { isTronAddress } from '../../utils.js'
1012
import { usePopoverControl } from './usePopoverControl.js'
1113
import { WarningCard } from './WarningCard.js'
1214

@@ -37,15 +39,26 @@ interface AddressTagProps {
3739
const AddressTag = memo<AddressTagProps>(function AddressTag({ address, text }) {
3840
const { classes } = useStyles()
3941
const { open, anchorEl, iconRef, onMouseEnter, onMouseLeave } = usePopoverControl()
40-
const { data: isScam } = useQuery({
42+
const { data } = useQuery({
4143
queryKey: ['detect-address', address],
42-
queryFn: () => {
43-
if (isValidAddress(address)) return PluginScamRPC.checkAddress(address)
44-
if (isSolAddress(address)) return false
45-
return false
44+
queryFn: async () => {
45+
if (isValidAddress(address)) {
46+
return { isScam: await PluginScamRPC.checkAddress(address), provider: SecurityProvider.ScamSniffer }
47+
}
48+
if (isSolAddress(address))
49+
return {
50+
isScam: await GoPlusLabs.checkIfAddressIsScam('solana', address),
51+
provider: SecurityProvider.GoPlus,
52+
}
53+
if (isTronAddress(address))
54+
return {
55+
isScam: GoPlusLabs.checkIfAddressIsScam('tron', address),
56+
provider: SecurityProvider.GoPlus,
57+
}
58+
return { isScam: false, provider: null }
4659
},
4760
})
48-
if (!isScam) return text
61+
if (!data?.isScam) return text
4962
return (
5063
<span className={classes.text}>
5164
<Icons.Danger
@@ -59,6 +72,7 @@ const AddressTag = memo<AddressTagProps>(function AddressTag({ address, text })
5972
<ShadowRootPopper open={open} anchorEl={anchorEl}>
6073
<WarningCard
6174
address={address}
75+
securityProvider={data.provider!}
6276
onMouseEnter={onMouseEnter}
6377
onMouseLeave={onMouseLeave}
6478
onClick={(e) => e.stopPropagation()}
@@ -77,7 +91,8 @@ export const TextModifier = memo<TextModifierProps>(function TextModifier({ fall
7791
const addresses = useMemo(() => {
7892
const evmAddresses = fullText.match(EVM_ADDRESS) || []
7993
const solAddresses = fullText.match(SOLANA_ADDRESS) || []
80-
return [...evmAddresses, ...solAddresses]
94+
const tronAddresses = fullText.match(TRON_ADDRESS) || []
95+
return [...evmAddresses, ...solAddresses, ...tronAddresses]
8196
}, [fullText])
8297

8398
const segments = useMemo(() => {

packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx

+22-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Icons } from '@masknet/icons'
44
import { LoadingBase, makeStyles } from '@masknet/theme'
55
import { Button, Typography } from '@mui/material'
66
import { memo, useState, type HTMLProps } from 'react'
7+
import { SecurityProvider } from '../../constants.js'
78

89
const useStyles = makeStyles()((theme) => ({
910
card: {
@@ -88,15 +89,17 @@ const useStyles = makeStyles()((theme) => ({
8889
fontWeight: 700,
8990
lineHeight: '16px',
9091
backgroundColor: theme.palette.maskColor.danger,
92+
color: theme.palette.maskColor.white,
9193
},
9294
}))
9395

9496
interface Props extends HTMLProps<HTMLDivElement> {
9597
link?: string
9698
address?: string
99+
securityProvider: SecurityProvider
97100
}
98101

99-
export const WarningCard = memo(function WarningCard({ link, address, ...rest }: Props) {
102+
export const WarningCard = memo(function WarningCard({ link, address, securityProvider, ...rest }: Props) {
100103
const { classes, cx } = useStyles()
101104
const [isReporting] = useState(false)
102105
return (
@@ -106,14 +109,24 @@ export const WarningCard = memo(function WarningCard({ link, address, ...rest }:
106109
<Icons.Danger className={classes.icon} size={24} />
107110
<Typography className={classes.name}>{t`Scam Warning`}</Typography>
108111
</div>
109-
<div className={classes.provider}>
110-
<Typography>
111-
<Trans>
112-
Powered by <span className={classes.providerName}>Scamsniffer</span>
113-
</Trans>
114-
</Typography>
115-
<Icons.ScamSniffer size={24} />
116-
</div>
112+
{securityProvider === SecurityProvider.GoPlus ?
113+
<div className={classes.provider}>
114+
<Typography>
115+
<Trans>
116+
Powered by <span className={classes.providerName}>Go+</span>
117+
</Trans>
118+
</Typography>
119+
<Icons.GoPlus size={24} />
120+
</div>
121+
: <div className={classes.provider}>
122+
<Typography>
123+
<Trans>
124+
Powered by <span className={classes.providerName}>Scamsniffer</span>
125+
</Trans>
126+
</Typography>
127+
<Icons.ScamSniffer size={24} />
128+
</div>
129+
}
117130
</div>
118131
<div className={classes.content}>
119132
{link ?

packages/plugins/ScamWarning/src/Worker/rpc.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ export async function checkUrl(url: string) {
4949

5050
export async function checkAddress(address: string) {
5151
const detector = getDetector()
52-
return true // detector.checkAddressInBlacklist(address)
52+
const result = await detector.checkAddressInBlacklist(address)
53+
return !!result
5354
}

packages/plugins/ScamWarning/src/constants.ts

+6
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ export const PLUGIN_NAME = 'ScamWarning'
66

77
export const EVM_ADDRESS = /(^|\s)(0x[a-fA-F0-9]{40})/gu
88
export const SOLANA_ADDRESS = /(^|\s)([1-9A-HJ-NP-Za-km-z]{32,44})/gu
9+
export const TRON_ADDRESS = /(^|\s)(T[A-Za-z1-9]{33})/gu
10+
11+
export enum SecurityProvider {
12+
ScamSniffer = 'ScamSniffer',
13+
GoPlus = 'GoPlus',
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { EVM_ADDRESS, SOLANA_ADDRESS } from './constants.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isTronAddress(address: string) {
2+
return !!address.match(address)
3+
}

0 commit comments

Comments
 (0)