1/** 2 * Chatroom moderation audit log â strikes, timeouts, bans, mod deletes. 3 * Writes to Firebase so mods can review what triggered auto-moderation. 4 */ 5window.ChatroomModerationLog = (function () { 6 'use strict'; 7 8 var LOG_PATH = 'chatroom/moderationLog'; 9 var MAX_PREVIEW = 240; 10 var DEFAULT_FETCH_LIMIT = 80; 11 12 function currentUsername() { 13 if (window.__sharedUsername && typeof window.__sharedUsername.get === 'function') { 14 return window.__sharedUsername.get() || 'guest'; 15 } 16 return 'guest'; 17 } 18 19 function trimPreview(text) { 20 if (!text || typeof text !== 'string') return ''; 21 var s = text.replace(/\s+/g, ' ').trim(); 22 if (s.length <= MAX_PREVIEW) return s; 23 return s.slice(0, MAX_PREVIEW) + '\u2026'; 24 } 25 26 function push(entry) { 27 if (typeof firebase === 'undefined' || !firebase.database) { 28 console.warn('[ChatroomModerationLog]', entry.type, entry); 29 return Promise.resolve(false); 30 } 31 var payload = Object.assign({ timestamp: Date.now() }, entry); 32 return firebase.database().ref(LOG_PATH).push().set(payload).then(function () { 33 console.log('[ChatroomModerationLog]', payload.type, payload); 34 return true; 35 }).catch(function (err) { 36 console.error('[ChatroomModerationLog] write failed:', err, payload); 37 return false; 38 }); 39 } 40 41 function logStrike(opts) { 42 return push({ 43 type: 'strike', 44 source: 'auto', 45 username: opts.username || currentUsername(), 46 strikeCount: opts.strikeCount || 0, 47 matchedRule: opts.matchedRule || '', 48 messagePreview: trimPreview(opts.messageText || ''), 49 channel: opts.channel || '', 50 isDm: opts.isDm === true, 51 timeoutUntil: opts.timeoutUntil || null 52 }); 53 } 54 55 function logTimeoutBlock(opts) { 56 return push({ 57 type: 'timeout_block', 58 source: 'auto', 59 username: opts.username || currentUsername(), 60 strikeCount: opts.strikeCount || 0, 61 channel: opts.channel || '', 62 isDm: opts.isDm === true, 63 timeoutUntil: opts.timeoutUntil || null, 64 remainingMs: opts.remainingMs || 0 65 }); 66 } 67 68 function logIpBan(opts) { 69 return push({ 70 type: 'ip_ban', 71 source: 'auto', 72 username: opts.username || currentUsername(), 73 strikeCount: 3, 74 matchedRule: opts.matchedRule || '', 75 messagePreview: trimPreview(opts.messageText || ''), 76 channel: opts.channel || '', 77 isDm: opts.isDm === true, 78 ip: opts.ip || '' 79 }); 80 } 81 82 function logMessageDelete(opts) { 83 return push({ 84 type: 'message_delete', 85 source: opts.source || 'moderator', 86 username: opts.messageAuthor || '', 87 actor: opts.actor || currentUsername(), 88 messagePreview: trimPreview(opts.messageText || ''), 89 messageId: opts.messageId || '', 90 channel: opts.channel || '', 91 isDm: opts.isDm === true 92 }); 93 } 94 95 function logChannelDelete(opts) { 96 return push({ 97 type: 'channel_delete', 98 source: 'moderator', 99 username: opts.actor || currentUsername(), 100 actor: opts.actor || currentUsername(), 101 channel: opts.channel || '' 102 }); 103 } 104 105 function fetchRecent(limit) { 106 var max = typeof limit === 'number' && limit > 0 ? limit : DEFAULT_FETCH_LIMIT; 107 if (typeof firebase === 'undefined' || !firebase.database) { 108 return Promise.resolve([]); 109 } 110 return firebase.database().ref(LOG_PATH) 111 .orderByChild('timestamp') 112 .limitToLast(max) 113 .once('value') 114 .then(function (snapshot) { 115 var rows = []; 116 if (!snapshot.exists()) return rows; 117 snapshot.forEach(function (child) { 118 var val = child.val(); 119 if (val && typeof val === 'object') { 120 val.id = child.key; 121 rows.push(val); 122 } 123 }); 124 rows.sort(function (a, b) { 125 return (b.timestamp || 0) - (a.timestamp || 0); 126 }); 127 return rows; 128 }) 129 .catch(function (err) { 130 console.error('[ChatroomModerationLog] fetch failed:', err); 131 return []; 132 }); 133 } 134 135 function formatTypeLabel(type) { 136 var map = {
137 strike: 'Strike', 138 timeout_block: 'Timeout block', 139 ip_ban: 'IP ban', 140 message_delete: 'Message deleted', 141 channel_delete: 'Channel deleted', 142 admin_clear: 'Admin cleared strikes' 143 }; 144 return map[type] || type || 'Event'; 145 } 146 147 function formatTimestamp(ts) { 148 if (!ts) return ''; 149 try { 150 return new Date(ts).toLocaleString(); 151 } catch (e) { 152 return String(ts); 153 } 154 } 155 156 function formatDuration(ms) { 157 if (!ms || ms <= 0) return '0s'; 158 if (ms < 60000) return Math.ceil(ms / 1000) + 's'; 159 var mins = Math.ceil(ms / 60000); 160 return mins + ' min'; 161 } 162 163 function describeEntry(entry) { 164 if (!entry) return ''; 165 var parts = []; 166 var where = entry.isDm ? 'DM' : (entry.channel ? '#' + entry.channel : 'channel'); 167 if (entry.type === 'strike') { 168 parts.push('Strike ' + (entry.strikeCount || '?') + '/3'); 169 if (entry.matchedRule) parts.push('matched rule: "' + entry.matchedRule + '"'); 170 if (entry.messagePreview) parts.push('message: "' + entry.messagePreview + '"'); 171 parts.push('in ' + where); 172 if (entry.timeoutUntil) { 173 parts.push('muted for ' + formatDuration(entry.timeoutUntil - (entry.timestamp || Date.now()))); 174 } 175 } else if (entry.type === 'timeout_block') { 176 parts.push('Tried to send while muted'); 177 parts.push('(' + formatDuration(entry.remainingMs) + ' left)'); 178 parts.push('in ' + where); 179 } else if (entry.type === 'ip_ban') { 180 parts.push('3 strikes: IP banned'); 181 if (entry.matchedRule) parts.push('matched rule: "' + entry.matchedRule + '"'); 182 if (entry.messagePreview) parts.push('message: "' + entry.messagePreview + '"'); 183 if (entry.ip) parts.push('IP: ' + entry.ip); 184 } else if (entry.type === 'message_delete') { 185 parts.push('Deleted message by ' + (entry.username || 'unknown')); 186 if (entry.actor && entry.actor !== entry.username) parts.push('(by ' + entry.actor + ')'); 187 if (entry.messagePreview) parts.push('"' + entry.messagePreview + '"'); 188 parts.push('in ' + where); 189 } else if (entry.type === 'channel_delete') { 190 parts.push('Deleted channel #' + (entry.channel || '?')); 191 if (entry.actor) parts.push('by ' + entry.actor); 192 } else if (entry.type === 'admin_clear') { 193 parts.push('Strikes cleared for ' + (entry.username || 'unknown')); 194 if (entry.actor) parts.push('by ' + entry.actor); 195 if (entry.note) parts.push(entry.note); 196 } 197 return parts.join(' · '); 198 } 199 200 return { 201 logStrike: logStrike, 202 logTimeoutBlock: logTimeoutBlock, 203 logIpBan: logIpBan, 204 logMessageDelete: logMessageDelete, 205 logChannelDelete: logChannelDelete, 206 fetchRecent: fetchRecent, 207 formatTypeLabel: formatTypeLabel, 208 formatTimestamp: formatTimestamp, 209 describeEntry: describeEntry 210 }; 211})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.