a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols
at master 153 lines 4.2 kB view raw
1import { apds } from 'apds' 2 3const MOD_KEY = 'moderation' 4const DEFAULT_STATE = { 5 mutedAuthors: [], 6 hiddenHashes: [], 7 mutedWords: [], 8 blockedAuthors: [] 9} 10 11let cachedState = null 12let cachedAt = 0 13const CACHE_TTL_MS = 2000 14 15const uniq = (list) => Array.from(new Set(list)) 16 17const cleanList = (list) => uniq( 18 (Array.isArray(list) ? list : []) 19 .map(item => (typeof item === 'string' ? item.trim() : '')) 20 .filter(Boolean) 21) 22 23const normalizeState = (state) => { 24 const base = state && typeof state === 'object' ? state : {} 25 return { 26 mutedAuthors: cleanList(base.mutedAuthors), 27 hiddenHashes: cleanList(base.hiddenHashes), 28 mutedWords: cleanList(base.mutedWords).map(word => word.toLowerCase()), 29 blockedAuthors: cleanList(base.blockedAuthors) 30 } 31} 32 33const parseState = (raw) => { 34 if (!raw) { return normalizeState(DEFAULT_STATE) } 35 try { 36 return normalizeState(JSON.parse(raw)) 37 } catch { 38 return normalizeState(DEFAULT_STATE) 39 } 40} 41 42export const splitTextList = (text) => { 43 if (!text) { return [] } 44 return text 45 .split(/\r?\n/) 46 .map(line => line.trim()) 47 .filter(Boolean) 48} 49 50export const getModerationState = async () => { 51 const now = Date.now() 52 if (cachedState && now - cachedAt < CACHE_TTL_MS) { 53 return cachedState 54 } 55 const stored = await apds.get(MOD_KEY) 56 cachedState = parseState(stored) 57 cachedAt = now 58 return cachedState 59} 60 61export const saveModerationState = async (nextState) => { 62 const normalized = normalizeState(nextState) 63 cachedState = normalized 64 cachedAt = Date.now() 65 await apds.put(MOD_KEY, JSON.stringify(normalized)) 66 return normalized 67} 68 69const updateState = async (updateFn) => { 70 const current = await getModerationState() 71 return saveModerationState(updateFn(current)) 72} 73 74export const addMutedAuthor = async (author) => { 75 if (!author) { return getModerationState() } 76 return updateState(state => ({ 77 ...state, 78 mutedAuthors: uniq([...state.mutedAuthors, author]) 79 })) 80} 81 82export const removeMutedAuthor = async (author) => { 83 if (!author) { return getModerationState() } 84 return updateState(state => ({ 85 ...state, 86 mutedAuthors: state.mutedAuthors.filter(item => item !== author) 87 })) 88} 89 90export const addBlockedAuthor = async (author) => { 91 if (!author) { return getModerationState() } 92 const next = await updateState(state => ({ 93 ...state, 94 blockedAuthors: uniq([...state.blockedAuthors, author]) 95 })) 96 let purge = null 97 if (typeof apds.purgeAuthor === 'function') { 98 purge = await apds.purgeAuthor(author) 99 } 100 return { state: next, purge } 101} 102 103export const removeBlockedAuthor = async (author) => { 104 if (!author) { return getModerationState() } 105 return updateState(state => ({ 106 ...state, 107 blockedAuthors: state.blockedAuthors.filter(item => item !== author) 108 })) 109} 110 111export const addHiddenHash = async (hash) => { 112 if (!hash) { return getModerationState() } 113 return updateState(state => ({ 114 ...state, 115 hiddenHashes: uniq([...state.hiddenHashes, hash]) 116 })) 117} 118 119export const removeHiddenHash = async (hash) => { 120 if (!hash) { return getModerationState() } 121 return updateState(state => ({ 122 ...state, 123 hiddenHashes: state.hiddenHashes.filter(item => item !== hash) 124 })) 125} 126 127export const shouldHideMessage = async ({ author, hash, body }) => { 128 const state = await getModerationState() 129 if (author && state.blockedAuthors.includes(author)) { 130 return { hidden: true, reason: 'Blocked author', code: 'blocked-author' } 131 } 132 if (author && state.mutedAuthors.includes(author)) { 133 return { hidden: true, reason: 'Muted author', code: 'muted-author' } 134 } 135 if (hash && state.hiddenHashes.includes(hash)) { 136 return { hidden: true, reason: 'Hidden message', code: 'hidden-hash' } 137 } 138 if (body && state.mutedWords.length) { 139 const lowered = body.toLowerCase() 140 for (const word of state.mutedWords) { 141 if (word && lowered.includes(word)) { 142 return { hidden: true, reason: 'Filtered keyword', code: 'muted-word', word } 143 } 144 } 145 } 146 return { hidden: false } 147} 148 149export const isBlockedAuthor = async (author) => { 150 if (!author || author.length !== 44) { return false } 151 const state = await getModerationState() 152 return state.blockedAuthors.includes(author) 153}