a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
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}