UNPKG

peertube-plugin-german-ai-mod

Version:

Local AI moderation for German comments using ml6team DistilBERT and deepset BERT models

417 lines (367 loc) 15.5 kB
async function register ({ registerClientRoute, registerHook, peertubeHelpers }) { const { notifier } = peertubeHelpers // ФУНКЦИЯ ПРОВЕРКИ ДОСТУПА (для хука меню) async function checkModerationAccess () { try { const baseRouter = peertubeHelpers.getBaseRouterRoute() const authHeader = peertubeHelpers.getAuthHeader() const response = await fetch(`${baseRouter}/check-access`, { headers: authHeader || {} }) if (response.ok) { const accessData = await response.json() return { hasAccess: accessData.allowed, isRootAdmin: accessData.isRootAdmin || false, user: accessData.user } } } catch (error) { console.error('German AI Mod: Access check failed:', error) } return { hasAccess: false, isRootAdmin: false } } // Add link to left menu registerHook({ target: 'filter:left-menu.links.create.result', handler: async (defaultLinks) => { console.log('German AI Mod: Processing menu links via hook...') try { // Check if user is logged in if (!peertubeHelpers.isLoggedIn()) { console.log('German AI Mod: User not logged in, returning default links') return defaultLinks } // Check if user can moderate (root admin or channel owner) const accessData = await checkModerationAccess() if (!accessData.hasAccess) { console.log('German AI Mod: No access to moderation, returning default links') return defaultLinks } console.log('German AI Mod: Adding menu section for user:', accessData.user?.username) // Add moderation link for authenticated users with access const moderationSection = { key: 'german-ai-mod', title: 'AI-Moderation', links: [ { icon: 'moderation', // Use moderation icon from PeerTube label: 'Kommentar-Moderation', path: '/p/admin/moderation', isPrimaryButton: false } ] } // Add section to menu const updatedLinks = [ ...defaultLinks, moderationSection ] console.log('German AI Mod: Menu section added successfully:', moderationSection) return updatedLinks } catch (error) { console.error('German AI Mod: Error in menu hook:', error) return defaultLinks } } }) // Register admin route for comment moderation registerClientRoute({ route: 'admin/moderation', onMount: async ({ rootEl }) => { // Check if user is logged in if (!peertubeHelpers.isLoggedIn()) { rootEl.innerHTML = ` <div class="margin-content col-md-12 col-xl-8" style="padding-top: 30px;"> <div style="text-align: center; padding: 40px;"> <h2>Zugriff verweigert</h2> <p>Bitte melden Sie sich an, um Kommentare zu moderieren.</p> </div> </div> ` return } const authHeader = peertubeHelpers.getAuthHeader() // Load moderation interface (access control handled by API) loadModerationInterface(rootEl, authHeader) } }) // Format date with timezone support function formatDate (dateString) { if (!dateString) return '-' try { const date = new Date(dateString) // Use toLocaleString with timezone options for proper formatting return date.toLocaleString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }) } catch (err) { return dateString } } async function loadModerationInterface (rootEl, authHeader) { // Load videos for filtering let videos = [] let isRootAdmin = false const baseRouter = peertubeHelpers.getBaseRouterRoute() try { const videosResponse = await fetch(`${baseRouter}/my-videos`, { headers: authHeader || {} }) if (!videosResponse.ok) { if (videosResponse.status === 401 || videosResponse.status === 403) { rootEl.innerHTML = ` <div class="margin-content col-md-12 col-xl-8" style="padding-top: 30px;"> <div style="text-align: center; padding: 40px;"> <h2>Zugriff verweigert</h2> <p>Sie haben keine Berechtigung, Kommentare zu moderieren.</p> </div> </div> ` return } throw new Error(`HTTP ${videosResponse.status}`) } const videosData = await videosResponse.json() videos = videosData.videos || [] // Check if user is root admin const commentsResponse = await fetch(`${baseRouter}/blocked-comments?page=1&limit=1`, { headers: authHeader || {} }) if (commentsResponse.ok) { const commentsData = await commentsResponse.json() isRootAdmin = commentsData.isRootAdmin || false } } catch (err) { console.error('Error loading videos:', err) rootEl.innerHTML = ` <div class="margin-content col-md-12 col-xl-8" style="padding-top: 30px;"> <div style="text-align: center; padding: 40px;"> <h2>Fehler</h2> <p>Fehler beim Laden der Seite: ${err.message}</p> </div> </div> ` return } rootEl.innerHTML = ` <div class="margin-content col-md-12 col-xl-10" style="padding-top: 30px;"> <h1>AI-Moderation: Blockierte Kommentare</h1> ${!isRootAdmin ? '<p style="color: #856404; background: #fff3cd; padding: 10px; border-radius: 4px; margin-bottom: 20px;">Sie sehen nur Kommentare zu Ihren eigenen Videos.</p>' : ''} <div style="margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 15px; align-items: center;"> <div> <strong>Filter:</strong> <label style="margin-left: 10px;"> <input type="radio" name="filter" value="all" checked> Alle </label> <label style="margin-left: 10px;"> <input type="radio" name="filter" value="toxic"> Toxisch </label> <label style="margin-left: 10px;"> <input type="radio" name="filter" value="non_german"> Nicht-Deutsch </label> <label style="margin-left: 10px;"> <input type="radio" name="filter" value="pending"> Ausstehend </label> <label style="margin-left: 10px;"> <input type="radio" name="filter" value="approved"> Genehmigt </label> </div> ${videos.length > 0 ? ` <div style="margin-left: auto;"> <label> <strong>Video:</strong> <select id="video-filter" style="margin-left: 10px; padding: 5px;"> <option value="">Alle Videos</option> ${videos.map(v => `<option value="${v.id}">${escapeHtml(v.name || `Video ${v.id}`)}</option>`).join('')} </select> </label> </div> ` : ''} </div> <div id="comments-list" style="margin-top: 20px;"> <p>Lade Kommentare...</p> </div> </div> ` const filterRadios = rootEl.querySelectorAll('input[name="filter"]') filterRadios.forEach(radio => { radio.addEventListener('change', () => { const videoFilter = rootEl.querySelector('#video-filter') loadComments(rootEl, authHeader, radio.value, videoFilter ? videoFilter.value : '') }) }) const videoFilter = rootEl.querySelector('#video-filter') if (videoFilter) { videoFilter.addEventListener('change', () => { const selectedFilter = rootEl.querySelector('input[name="filter"]:checked') loadComments(rootEl, authHeader, selectedFilter ? selectedFilter.value : 'all', videoFilter.value) }) } loadComments(rootEl, authHeader, 'all', '') } async function loadComments (rootEl, authHeader, filter, videoId = '') { const listEl = rootEl.querySelector('#comments-list') listEl.innerHTML = '<p>Lade Kommentare...</p>' const baseRouter = peertubeHelpers.getBaseRouterRoute() try { let url = `${baseRouter}/blocked-comments?page=1&limit=100` if (filter === 'toxic' || filter === 'non_german') { url += `&blockReason=${filter}` } else if (filter === 'pending') { url += '&approved=false' } else if (filter === 'approved') { url += '&approved=true' } if (videoId) { url += `&videoId=${videoId}` } const response = await fetch(url, { headers: authHeader || {} }) if (!response.ok) { if (response.status === 401 || response.status === 403) { listEl.innerHTML = ` <div style="text-align: center; padding: 40px;"> <h3>Zugriff verweigert</h3> <p>Sie haben keine Berechtigung, Kommentare zu moderieren.</p> </div> ` return } throw new Error(`HTTP ${response.status}`) } const data = await response.json() if (data.data.length === 0) { listEl.innerHTML = '<p>Keine blockierten Kommentare gefunden.</p>' return } listEl.innerHTML = ` <table style="width: 100%; border-collapse: collapse;"> <thead> <tr style="border-bottom: 2px solid #ddd;"> <th style="padding: 10px; text-align: left;">Text</th> <th style="padding: 10px; text-align: left;">Video</th> <th style="padding: 10px; text-align: left;">Grund</th> <th style="padding: 10px; text-align: left;">Sprache</th> <th style="padding: 10px; text-align: left;">Scores</th> <th style="padding: 10px; text-align: left;">Benutzer</th> <th style="padding: 10px; text-align: left;">Datum</th> <th style="padding: 10px; text-align: left;">Aktionen</th> </tr> </thead> <tbody id="comments-tbody"> </tbody> </table> ` const tbody = listEl.querySelector('#comments-tbody') data.data.forEach(comment => { const row = document.createElement('tr') row.style.borderBottom = '1px solid #eee' const reasonBadge = comment.blockReason === 'toxic' ? '<span style="background: #dc3545; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">Toxisch</span>' : '<span style="background: #ffc107; color: black; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">Nicht-Deutsch</span>' const scoreText = comment.score !== null ? `Tox: ${comment.score.toFixed(3)}${comment.hateScore !== null ? `, Hate: ${comment.hateScore.toFixed(3)}` : ''}` : '-' const approvedBadge = comment.approved ? '<span style="background: #28a745; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">Genehmigt</span>' : '<span style="background: #6c757d; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">Ausstehend</span>' const videoName = comment.videoUuid ? `<a href="/videos/watch/${comment.videoUuid}" target="_blank" style="color: #007bff; text-decoration: none;">Video ${comment.videoId || comment.videoUuid}</a>` : `Video ${comment.videoId || '-'}` row.innerHTML = ` <td style="padding: 10px; max-width: 300px;"> <div style="max-height: 60px; overflow: hidden; text-overflow: ellipsis;"> ${escapeHtml(comment.text)} </div> </td> <td style="padding: 10px; font-size: 0.9em;">${videoName}</td> <td style="padding: 10px;">${reasonBadge}</td> <td style="padding: 10px;">${comment.language || '-'}</td> <td style="padding: 10px; font-size: 0.9em;">${scoreText}</td> <td style="padding: 10px;">${comment.userUsername || '-'}</td> <td style="padding: 10px; font-size: 0.9em;">${formatDate(comment.createdAt)}</td> <td style="padding: 10px;"> ${comment.approved ? '' : `<button class="btn btn-success btn-sm" onclick="approveComment(${comment.id}, this)" style="margin-right: 5px;">Genehmigen</button>`} <button class="btn btn-danger btn-sm" onclick="deleteComment(${comment.id}, this)">Löschen</button> ${approvedBadge} </td> ` tbody.appendChild(row) }) // Add global functions for buttons window.approveComment = async function (id, buttonEl) { if (!confirm('Kommentar wirklich genehmigen? Er wird dann veröffentlicht.')) { return } buttonEl.disabled = true buttonEl.textContent = 'Wird genehmigt...' const baseRouter = peertubeHelpers.getBaseRouterRoute() try { const response = await fetch(`${baseRouter}/blocked-comments/${id}/approve`, { method: 'POST', headers: authHeader || {} }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } notifier.success('Kommentar wurde genehmigt') const selectedFilter = rootEl.querySelector('input[name="filter"]:checked') const videoFilter = rootEl.querySelector('#video-filter') loadComments(rootEl, authHeader, selectedFilter ? selectedFilter.value : 'all', videoFilter ? videoFilter.value : '') } catch (err) { notifier.error('Fehler beim Genehmigen: ' + err.message) buttonEl.disabled = false buttonEl.textContent = 'Genehmigen' } } window.deleteComment = async function (id, buttonEl) { if (!confirm('Kommentar wirklich löschen?')) { return } buttonEl.disabled = true buttonEl.textContent = 'Wird gelöscht...' const baseRouter = peertubeHelpers.getBaseRouterRoute() try { const response = await fetch(`${baseRouter}/blocked-comments/${id}`, { method: 'DELETE', headers: authHeader || {} }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } notifier.success('Kommentar wurde gelöscht') const selectedFilter = rootEl.querySelector('input[name="filter"]:checked') const videoFilter = rootEl.querySelector('#video-filter') loadComments(rootEl, authHeader, selectedFilter ? selectedFilter.value : 'all', videoFilter ? videoFilter.value : '') } catch (err) { notifier.error('Fehler beim Löschen: ' + err.message) buttonEl.disabled = false buttonEl.textContent = 'Löschen' } } } catch (err) { listEl.innerHTML = `<p style="color: red;">Fehler beim Laden: ${err.message}</p>` } } function escapeHtml (text) { const div = document.createElement('div') div.textContent = text return div.innerHTML } } function unregister () { // Cleanup if needed } // ES module export for PeerTube client plugins export { register, unregister }