PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/moderationLog.js

js pupwife.com collected 2026-09-30 15:06:52 UTC 7,883 bytes, 211 lines download raw bytes

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.