feat: add badge for suspicious and spam users
This commit is contained in:
@@ -16,6 +16,7 @@ import Nip05 from '../Nip05'
|
||||
import NoteOptions from '../NoteOptions'
|
||||
import ParentNotePreview from '../ParentNotePreview'
|
||||
import TranslateButton from '../TranslateButton'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
import Username from '../Username'
|
||||
import CommunityDefinition from './CommunityDefinition'
|
||||
@@ -125,6 +126,7 @@ export default function Note({
|
||||
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
|
||||
/>
|
||||
<FollowingBadge pubkey={event.pubkey} />
|
||||
<TrustScoreBadge pubkey={event.pubkey} />
|
||||
<ClientTag event={event} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
|
||||
@@ -19,13 +19,14 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NotFound from '../NotFound'
|
||||
import SearchInput from '../SearchInput'
|
||||
import TextWithEmojis from '../TextWithEmojis'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
import AvatarWithLightbox from './AvatarWithLightbox'
|
||||
import BannerWithLightbox from './BannerWithLightbox'
|
||||
import FollowedBy from './FollowedBy'
|
||||
import Followings from './Followings'
|
||||
import ProfileFeed from './ProfileFeed'
|
||||
import Relays from './Relays'
|
||||
import TextWithEmojis from '../TextWithEmojis'
|
||||
import AvatarWithLightbox from './AvatarWithLightbox'
|
||||
import BannerWithLightbox from './BannerWithLightbox'
|
||||
|
||||
export default function Profile({ id }: { id?: string }) {
|
||||
const { t } = useTranslation()
|
||||
@@ -143,6 +144,7 @@ export default function Profile({ id }: { id?: string }) {
|
||||
emojis={emojis}
|
||||
className="text-xl font-semibold truncate select-text"
|
||||
/>
|
||||
<TrustScoreBadge pubkey={pubkey} />
|
||||
{isFollowingYou && (
|
||||
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
|
||||
{t('Follows you')}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useMemo } from 'react'
|
||||
import FollowButton from '../FollowButton'
|
||||
import Nip05 from '../Nip05'
|
||||
import ProfileAbout from '../ProfileAbout'
|
||||
import TextWithEmojis from '../TextWithEmojis'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
import { SimpleUserAvatar } from '../UserAvatar'
|
||||
|
||||
export default function ProfileCard({ userId }: { userId: string }) {
|
||||
@@ -18,7 +20,14 @@ export default function ProfileCard({ userId }: { userId: string }) {
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold truncate">{username}</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<TextWithEmojis
|
||||
text={username || ''}
|
||||
emojis={emojis}
|
||||
className="text-lg font-semibold truncate"
|
||||
/>
|
||||
<TrustScoreBadge pubkey={pubkey} />
|
||||
</div>
|
||||
<Nip05 pubkey={pubkey} />
|
||||
</div>
|
||||
{about && (
|
||||
|
||||
64
src/components/TrustScoreBadge/index.tsx
Normal file
64
src/components/TrustScoreBadge/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import trustScoreService from '@/services/trust-score.service'
|
||||
import { AlertTriangle, ShieldAlert } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function TrustScoreBadge({
|
||||
pubkey,
|
||||
className
|
||||
}: {
|
||||
pubkey: string
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { pubkey: currentPubkey } = useNostr()
|
||||
const [percentile, setPercentile] = useState<number | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPubkey === pubkey) {
|
||||
setLoading(false)
|
||||
setPercentile(null)
|
||||
return
|
||||
}
|
||||
|
||||
const fetchScore = async () => {
|
||||
try {
|
||||
const data = await trustScoreService.fetchTrustScore(pubkey)
|
||||
if (data) {
|
||||
setPercentile(data.percentile)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch trust score:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchScore()
|
||||
}, [pubkey, currentPubkey])
|
||||
|
||||
if (loading || percentile === null) return null
|
||||
|
||||
// percentile < 50: likely spam (red alert)
|
||||
// percentile < 75: suspicious (yellow warning)
|
||||
if (percentile < 50) {
|
||||
return (
|
||||
<div title={t('Likely spam account (Trust score: {{percentile}}%)', { percentile })}>
|
||||
<ShieldAlert className={cn('!size-4 text-red-500', className)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (percentile < 75) {
|
||||
return (
|
||||
<div title={t('Suspicious account (Trust score: {{percentile}}%)', { percentile })}>
|
||||
<AlertTriangle className={cn('!size-4 text-yellow-600 dark:text-yellow-500', className)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { userIdToPubkey } from '@/lib/pubkey'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import FollowingBadge from '../FollowingBadge'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
|
||||
export default function UserItem({
|
||||
userId,
|
||||
@@ -32,6 +33,7 @@ export default function UserItem({
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
{showFollowingBadge && <FollowingBadge pubkey={pubkey} />}
|
||||
<TrustScoreBadge pubkey={pubkey} />
|
||||
</div>
|
||||
<Nip05 pubkey={userId} />
|
||||
</div>
|
||||
|
||||
@@ -550,6 +550,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'فشل إعادة النشر إلى المرحلات المثلى: {{error}}',
|
||||
'External Content': 'محتوى خارجي',
|
||||
Highlight: 'تسليط الضوء',
|
||||
'Optimal relays and {{count}} other relays': 'المرحلات المثلى و {{count}} مرحلات أخرى'
|
||||
'Optimal relays and {{count}} other relays': 'المرحلات المثلى و {{count}} مرحلات أخرى',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'حساب مشبوه للغاية (درجة الثقة: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشبوه (درجة الثقة: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +566,8 @@ export default {
|
||||
'Fehler beim Neuveröffentlichen auf optimale Relays: {{error}}',
|
||||
'External Content': 'Externer Inhalt',
|
||||
Highlight: 'Hervorheben',
|
||||
'Optimal relays and {{count}} other relays': 'Optimale Relays und {{count}} andere Relays'
|
||||
'Optimal relays and {{count}} other relays': 'Optimale Relays und {{count}} andere Relays',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Wahrscheinlich Spam-Konto (Vertrauenswert: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Verdächtiges Konto (Vertrauenswert: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,9 +547,14 @@ export default {
|
||||
'Optimal relays': 'Optimal relays',
|
||||
"Successfully republish to optimal relays (your write relays and mentioned users' read relays)":
|
||||
"Successfully republish to optimal relays (your write relays and mentioned users' read relays)",
|
||||
'Failed to republish to optimal relays: {{error}}': 'Failed to republish to optimal relays: {{error}}',
|
||||
'Failed to republish to optimal relays: {{error}}':
|
||||
'Failed to republish to optimal relays: {{error}}',
|
||||
'External Content': 'External Content',
|
||||
Highlight: 'Highlight',
|
||||
'Optimal relays and {{count}} other relays': 'Optimal relays and {{count}} other relays'
|
||||
'Optimal relays and {{count}} other relays': 'Optimal relays and {{count}} other relays',
|
||||
'Likely spam account (Trust score: {{percentile}}%)':
|
||||
'Likely spam account (Trust score: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)':
|
||||
'Suspicious account (Trust score: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +561,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Error al republicar en relays óptimos: {{error}}',
|
||||
'External Content': 'Contenido externo',
|
||||
Highlight: 'Destacado',
|
||||
'Optimal relays and {{count}} other relays': 'Relays óptimos y {{count}} otros relays'
|
||||
'Optimal relays and {{count}} other relays': 'Relays óptimos y {{count}} otros relays',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Probablemente cuenta spam (Puntuación de confianza: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Cuenta sospechosa (Puntuación de confianza: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +555,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'خطا در انتشار مجدد در رلههای بهینه: {{error}}',
|
||||
'External Content': 'محتوای خارجی',
|
||||
Highlight: 'برجستهسازی',
|
||||
'Optimal relays and {{count}} other relays': 'رلههای بهینه و {{count}} رله دیگر'
|
||||
'Optimal relays and {{count}} other relays': 'رلههای بهینه و {{count}} رله دیگر',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'احتمالاً حساب هرزنامه (امتیاز اعتماد: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشکوک (امتیاز اعتماد: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +564,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Échec de la republication sur les relais optimaux : {{error}}',
|
||||
'External Content': 'Contenu externe',
|
||||
Highlight: 'Surligner',
|
||||
'Optimal relays and {{count}} other relays': 'Relais optimaux et {{count}} autres relais'
|
||||
'Optimal relays and {{count}} other relays': 'Relais optimaux et {{count}} autres relais',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Compte probablement spam (Score de confiance: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Compte suspect (Score de confiance: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +556,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'इष्टतम रिले पर पुनः प्रकाशित करने में विफल: {{error}}',
|
||||
'External Content': 'बाहरी सामग्री',
|
||||
Highlight: 'हाइलाइट',
|
||||
'Optimal relays and {{count}} other relays': 'इष्टतम रिले और {{count}} अन्य रिले'
|
||||
'Optimal relays and {{count}} other relays': 'इष्टतम रिले और {{count}} अन्य रिले',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'संभावित स्पैम खाता (विश्वास स्कोर: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'संदिग्ध खाता (विश्वास स्कोर: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +551,8 @@ export default {
|
||||
'Nem sikerült újra közzétenni az optimális relay-eken: {{error}}',
|
||||
'External Content': 'Külső tartalom',
|
||||
Highlight: 'Kiemelés',
|
||||
'Optimal relays and {{count}} other relays': 'Optimális relay-ek és {{count}} másik relay'
|
||||
'Optimal relays and {{count}} other relays': 'Optimális relay-ek és {{count}} másik relay',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Valószínűleg spam fiók (Megbízhatósági pontszám: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Gyanús fiók (Megbízhatósági pontszám: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +560,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Errore nella ripubblicazione sui relay ottimali: {{error}}',
|
||||
'External Content': 'Contenuto esterno',
|
||||
Highlight: 'Evidenzia',
|
||||
'Optimal relays and {{count}} other relays': 'Relay ottimali e {{count}} altri relay'
|
||||
'Optimal relays and {{count}} other relays': 'Relay ottimali e {{count}} altri relay',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Probabile account spam (Punteggio di fiducia: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Account sospetto (Punteggio di fiducia: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +555,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': '最適なリレーへの再公開に失敗しました:{{error}}',
|
||||
'External Content': '外部コンテンツ',
|
||||
Highlight: 'ハイライト',
|
||||
'Optimal relays and {{count}} other relays': '最適なリレーと他の{{count}}個のリレー'
|
||||
'Optimal relays and {{count}} other relays': '最適なリレーと他の{{count}}個のリレー',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'スパムの可能性が高いアカウント(信頼スコア:{{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': '疑わしいアカウント(信頼スコア:{{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +555,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': '최적 릴레이에 재게시 실패: {{error}}',
|
||||
'External Content': '외부 콘텐츠',
|
||||
Highlight: '하이라이트',
|
||||
'Optimal relays and {{count}} other relays': '최적 릴레이 및 기타 {{count}}개 릴레이'
|
||||
'Optimal relays and {{count}} other relays': '최적 릴레이 및 기타 {{count}}개 릴레이',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': '스팸 계정 가능성 높음 (신뢰 점수: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': '의심스러운 계정 (신뢰 점수: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +561,8 @@ export default {
|
||||
'Nie udało się opublikować ponownie na optymalnych przekaźnikach: {{error}}',
|
||||
'External Content': 'Treść zewnętrzna',
|
||||
Highlight: 'Podświetl',
|
||||
'Optimal relays and {{count}} other relays': 'Optymalne przekaźniki i {{count}} innych przekaźników'
|
||||
'Optimal relays and {{count}} other relays': 'Optymalne przekaźniki i {{count}} innych przekaźników',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Prawdopodobnie konto spamowe (Wynik zaufania: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Podejrzane konto (Wynik zaufania: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +556,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Falha ao republicar nos relays ideais: {{error}}',
|
||||
'External Content': 'Conteúdo externo',
|
||||
Highlight: 'Marcação',
|
||||
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays'
|
||||
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +559,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Falha ao republicar nos relays ideais: {{error}}',
|
||||
'External Content': 'Conteúdo externo',
|
||||
Highlight: 'Destacar',
|
||||
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays'
|
||||
'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +561,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'Не удалось опубликовать в оптимальные релеи: {{error}}',
|
||||
'External Content': 'Внешний контент',
|
||||
Highlight: 'Выделить',
|
||||
'Optimal relays and {{count}} other relays': 'Оптимальные релеи и {{count}} других релеев'
|
||||
'Optimal relays and {{count}} other relays': 'Оптимальные релеи и {{count}} других релеев',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'Вероятно спам-аккаунт (Оценка доверия: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'Подозрительный аккаунт (Оценка доверия: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +548,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': 'เผยแพร่ซ้ำไปยังรีเลย์ที่เหมาะสมล้มเหลว: {{error}}',
|
||||
'External Content': 'เนื้อหาภายนอก',
|
||||
Highlight: 'ไฮไลต์',
|
||||
'Optimal relays and {{count}} other relays': 'รีเลย์ที่เหมาะสมและรีเลย์อื่น {{count}} รายการ'
|
||||
'Optimal relays and {{count}} other relays': 'รีเลย์ที่เหมาะสมและรีเลย์อื่น {{count}} รายการ',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': 'น่าจะเป็นบัญชีสแปม (คะแนนความน่าเชื่อถือ: {{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': 'บัญชีที่น่าสงสัย (คะแนนความน่าเชื่อถือ: {{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +543,8 @@ export default {
|
||||
'Failed to republish to optimal relays: {{error}}': '重新发布到最佳中继器失败:{{error}}',
|
||||
'External Content': '外部内容',
|
||||
Highlight: '高亮',
|
||||
'Optimal relays and {{count}} other relays': '最佳中继器和其他 {{count}} 个中继器'
|
||||
'Optimal relays and {{count}} other relays': '最佳中继器和其他 {{count}} 个中继器',
|
||||
'Likely spam account (Trust score: {{percentile}}%)': '疑似垃圾账号(信任分数:{{percentile}}%)',
|
||||
'Suspicious account (Trust score: {{percentile}}%)': '可疑账号(信任分数:{{percentile}}%)'
|
||||
}
|
||||
}
|
||||
|
||||
47
src/services/trust-score.service.ts
Normal file
47
src/services/trust-score.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import DataLoader from 'dataloader'
|
||||
|
||||
export interface TrustScoreData {
|
||||
percentile: number
|
||||
}
|
||||
|
||||
class TrustScoreService {
|
||||
static instance: TrustScoreService
|
||||
|
||||
private trustScoreDataLoader = new DataLoader<string, TrustScoreData | null>(async (userIds) => {
|
||||
return await Promise.all(
|
||||
userIds.map(async (userId) => {
|
||||
try {
|
||||
const res = await fetch(`https://fayan.jumble.social/${userId}`)
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
return { percentile: 0 }
|
||||
}
|
||||
return null
|
||||
}
|
||||
const data = await res.json()
|
||||
if (typeof data.percentile === 'number') {
|
||||
return { percentile: data.percentile }
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
constructor() {
|
||||
if (!TrustScoreService.instance) {
|
||||
TrustScoreService.instance = this
|
||||
}
|
||||
return TrustScoreService.instance
|
||||
}
|
||||
|
||||
async fetchTrustScore(userId: string): Promise<TrustScoreData | null> {
|
||||
return await this.trustScoreDataLoader.load(userId)
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TrustScoreService()
|
||||
|
||||
export default instance
|
||||
Reference in New Issue
Block a user