import React, { useCallback, useRef, useState } from 'react'; import { Badge, Box, Button, Icon, IconButton, Icons, Input, Scroll, Text, color, config, } from 'folds'; import { EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk'; import { Page, PageContent, PageHeader } from '../../components/page'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { SequenceCard } from '../../components/sequence-card'; import { SequenceCardStyle } from '../common-settings/styles.css'; // ── Policy event types ──────────────────────────────────────────────────────── const POLICY_USER_EVENT = 'm.policy.rule.user'; const POLICY_ROOM_EVENT = 'm.policy.rule.room'; const POLICY_SERVER_EVENT = 'm.policy.rule.server'; type PolicyRuleContent = { entity?: string; reason?: string; recommendation?: string; }; type PolicyEntry = { entity: string; reason: string; recommendation: string; stateKey: string; }; type PolicyTab = 'users' | 'rooms' | 'servers'; // ── Helpers ─────────────────────────────────────────────────────────────────── function isGlob(entity: string): boolean { return entity.includes('*') || entity.includes('?'); } function recommendationLabel(rec: string): string { if (rec === 'm.ban') return 'Ban'; return rec; } /** * Fetch all state events of a given type from a room's live forward state. * Uses the raw matrix-js-sdk `getStateEvents` API which accepts any string type. */ function getRoomPolicyEvents(room: Room, eventType: string): MatrixEvent[] { const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS); if (!state) return []; return state.getStateEvents(eventType); } function extractPolicyEntries(events: MatrixEvent[]): PolicyEntry[] { return events .map((e) => { const content = e.getContent(); return { entity: content.entity ?? '', reason: content.reason ?? '', recommendation: content.recommendation ?? '', stateKey: e.getStateKey() ?? '', }; }) .filter((entry) => entry.entity !== ''); } // ── Entry row ───────────────────────────────────────────────────────────────── function PolicyEntryRow({ entry }: { entry: PolicyEntry }) { const glob = isGlob(entry.entity); return ( {entry.entity} {glob && ( glob )} {recommendationLabel(entry.recommendation)} {entry.reason && ( {entry.reason} )} ); } // ── Tab button ──────────────────────────────────────────────────────────────── function TabButton({ label, count, active, onClick, }: { label: string; count: number; active: boolean; onClick: () => void; }) { return ( ); } // ── Main component ──────────────────────────────────────────────────────────── type PolicyListViewerProps = { requestClose: () => void; }; export function PolicyListViewer({ requestClose }: PolicyListViewerProps) { const mx = useMatrixClient(); const inputRef = useRef(null); const [roomIdInput, setRoomIdInput] = useState(''); const [activeTab, setActiveTab] = useState('users'); const [error, setError] = useState(); const [userEntries, setUserEntries] = useState([]); const [roomEntries, setRoomEntries] = useState([]); const [serverEntries, setServerEntries] = useState([]); const [loadedRoomId, setLoadedRoomId] = useState(); const handleLoad = useCallback(() => { const rawInput = (inputRef.current?.value ?? roomIdInput).trim(); if (!rawInput) { setError('Please enter a room ID or alias.'); return; } // Resolve alias to room ID using local cache let roomId = rawInput; if (rawInput.startsWith('#')) { const cachedId = mx.getRooms().find((r) => { const aliases = r.getAltAliases(); const canonical = r.getCanonicalAlias(); return aliases.includes(rawInput) || canonical === rawInput; })?.roomId; if (cachedId) { roomId = cachedId; } else { setError(`Cannot resolve alias "${rawInput}". Make sure you have joined that room.`); return; } } const room = mx.getRoom(roomId); if (!room) { setError(`Not joined to room "${roomId}". Join the policy list room first.`); setUserEntries([]); setRoomEntries([]); setServerEntries([]); setLoadedRoomId(undefined); return; } setUserEntries(extractPolicyEntries(getRoomPolicyEvents(room, POLICY_USER_EVENT))); setRoomEntries(extractPolicyEntries(getRoomPolicyEvents(room, POLICY_ROOM_EVENT))); setServerEntries(extractPolicyEntries(getRoomPolicyEvents(room, POLICY_SERVER_EVENT))); setLoadedRoomId(roomId); setError(undefined); }, [mx, roomIdInput]); const activeEntries = activeTab === 'users' ? userEntries : activeTab === 'rooms' ? roomEntries : serverEntries; return ( Policy Lists {/* Description */} About Policy lists are Matrix rooms containing ban rules managed by moderation bots (e.g. Draupnir). Enter a policy list room ID below to inspect its current rules. This is a read-only viewer — rule enforcement is handled by your moderation bot. {/* Room ID input */} Policy List Room setRoomIdInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleLoad(); }} placeholder="!roomId:server or #alias:server" variant={error ? 'Critical' : 'Secondary'} size="400" radii="300" style={{ flexGrow: 1 }} /> {error && ( {error} )} {loadedRoomId && ( Showing rules from:{' '} {loadedRoomId} )} {/* Rules viewer */} {loadedRoomId && ( Rules {/* Tabs */} setActiveTab('users')} /> setActiveTab('rooms')} /> setActiveTab('servers')} /> {/* Entry list */} {activeEntries.length === 0 ? ( No{' '} {activeTab === 'users' ? 'user' : activeTab === 'rooms' ? 'room' : 'server'}{' '} ban rules found. ) : ( activeEntries.map((entry) => ( )) )} )} ); }