# 10 — Raum-Administration & Moderation --- ## Inhaltsverzeichnis - [Power Levels vollständig](#power-levels-vollständig) - [Moderation: Kick, Ban, Unban](#moderation-kick-ban-unban) - [Server-Side Room Admin](#server-side-room-admin) - [Raum-Einstellungen komplett](#raum-einstellungen-komplett) - [Raum-Alias verwalten](#raum-alias-verwalten) - [Server ACL (IP-Sperren)](#server-acl-ip-sperren) - [Raum-Audit / State inspizieren](#raum-audit--state-inspizieren) - [Widgets / Bots verwalten](#widgets--bots-verwalten) - [Room Tombstone / Upgrade](#room-tombstone--upgrade) - [Reporting (Melden)](#reporting-melden) --- ## Power Levels vollständig ```dart // Power Level eines Users ändern await room.setPower('@alice:server', 50); // 0=User, 50=Mod, 100=Admin // Alle Power Levels auf einmal setzen await room.client.sendState( room.id, EventTypes.RoomPowerLevels, { // Welche Power-Level für Standard-Aktionen benötigt werden: 'ban': 50, 'kick': 50, 'invite': 0, 'redact': 50, 'events_default': 0, // Nachrichten senden 'state_default': 50, // State-Events ändern // Spezifische Event-Typen: 'events': { 'm.room.name': 50, 'm.room.topic': 50, 'm.room.avatar': 50, 'm.room.power_levels': 100, 'm.room.canonical_alias': 50, 'm.room.history_visibility': 100, 'm.room.encryption': 100, 'm.space.child': 50, }, // User-Overrides: 'users': { '@admin:server': 100, '@moderator:server': 50, }, 'users_default': 0, // Alle anderen User 'notifications': { 'room': 50, // @room mention power level }, }, stateKey: '', ); // Power Level lesen final plContent = room.getState(EventTypes.RoomPowerLevels)?.content; final usersMap = plContent?.tryGetMap('users') ?? {}; final adminPl = usersMap['@alice:server'] as int? ?? 0; // Berechtigungen prüfen room.ownPowerLevel; // int: eigener PL room.getPowerLevelByUserId('@alice:server'); // int room.canBan; // bool room.canKick; // bool room.canInvite; // bool room.canRedactEvent(event); // bool room.canSendEvent(EventTypes.Message); // bool room.canSendEvent(EventTypes.RoomName); // bool room.canChangeStateEvent(EventTypes.RoomTopic); // bool room.canChangeNameAndTopic; // bool room.canChangeJoinRules; // bool room.canChangeHistoryVisibility; // bool ``` --- ## Moderation: Kick, Ban, Unban ```dart // Kick (User wird entfernt, kann wieder beitreten) await room.kick('@alice:server', reason: 'Verstoß gegen Regeln'); // Ban (User kann nicht mehr beitreten) await room.ban('@alice:server', reason: 'Dauerhafter Ausschluss'); // Unban await room.unban('@alice:server'); // Alle gebannten User anzeigen final bannedMembers = room.getParticipants( membershipFilter: [Membership.ban], ); // Eigene Berechtigungen prüfen vor Moderation-Aktionen if (room.canKick) { await room.kick(userId); } if (room.canBan) { await room.ban(userId); } // User-PL prüfen (kann man diesen User kicken/bannen?) // Man kann nur User mit niedrigerem PL als dem eigenen moderieren. final theirPl = room.getPowerLevelByUserId(userId); final myPl = room.ownPowerLevel; final canModerate = myPl > theirPl; ``` --- ## Server-Side Room Admin > ⚠️ **Pyramid läuft auf Continuwuity** — Synapse Admin API (`/_synapse/admin/`) **existiert nicht!** > Für vollständige Continuwuity-Admin-Dokumentation: **[→ 13-server-admin-continuwuity.md](13-server-admin-continuwuity.md)** ```dart // Server-Admin-Status prüfen (Matrix-Standard, funktioniert überall) final isAdmin = await client.getIsAdmin(); // Für Continuwuity: Admin-Aktionen über Standard Matrix Client API // oder über den Admin-Room (#admins:steggi-matrix.work) // Power Level in einem Raum als Admin setzen: await client.setRoomStateWithKey( room.id, EventTypes.RoomPowerLevels, '', { 'users': {'@admin:server': 100}, 'users_default': 0, 'state_default': 50, 'events_default': 0, 'ban': 50, 'kick': 50, 'invite': 0, 'redact': 50, }, ); // Raum beitreten (Admin kann invite-only-Räume per API beitreten): await client.joinRoom(room.id); // Space-Child-Relation setzen (Channel einem Space hinzufügen): await client.setRoomStateWithKey( spaceId, EventTypes.SpaceChild, channelRoomId, {'via': [client.homeserver.host], 'suggested': false}, ); ``` ```bash # Continuwuity Admin HTTP API # Basis: https://server/_conduwuit/admin/v1/ # User zu Server-Admin machen: curl -X POST "https://server/_conduwuit/admin/v1/make_user_admin" \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"user_id": "@admin:server"}' # Power Level in Raum setzen (Standard Matrix API, mit Admin-Token): curl -X PUT "https://server/_matrix/client/v3/rooms/!roomId:server/state/m.room.power_levels/" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"users": {"@admin:server": 100}, "users_default": 0, "state_default": 50}' # Für Synapse (falls Server-Wechsel zu Synapse): # curl .../server/_synapse/admin/v1/rooms/{roomId} ← dann wieder relevant ``` --- ## Raum-Einstellungen komplett ```dart // Name await room.setName('Neuer Raumname'); // Thema await room.setDescription('Neues Thema'); // Avatar final mxcUri = await client.uploadContent(imageBytes, filename: 'avatar.png'); await room.setAvatar(mxcUri); // Join-Rules await room.setJoinRules(JoinRules.invite); // Privat await room.setJoinRules(JoinRules.public); // Öffentlich await room.setJoinRules(JoinRules.knock); // Beitrittsanfragen // Restricted (Nur Space-Mitglieder können beitreten) await room.setJoinRules( JoinRules.restricted, allow: [ SpaceRoomJoinRulesAllowInner( roomId: '!spaceId:server', type: SpaceRoomJoinRulesAllowInnerType.mRoomMembership, ), ], ); // History-Visibility await room.setHistoryVisibility(HistoryVisibility.shared); // .shared = alle sehen vergangene Nachrichten (nach Join) // .invited = sieht Nachrichten ab dem Einladungs-Zeitpunkt // .joined = sieht Nachrichten nur ab dem Beitritts-Zeitpunkt // .worldReadable = öffentlich lesbar (auch ohne Account) // Guest Access await room.setGuestAccess(GuestAccess.canJoin); // Gäste erlaubt await room.setGuestAccess(GuestAccess.forbidden); // Keine Gäste // E2EE aktivieren (einmalig, nicht deaktivierbar!) await room.enableEncryption(); // Notifications für @room: Welcher PL nötig? // → über Power Levels 'notifications.room' setzen ``` --- ## Raum-Alias verwalten ```dart // Alias erstellen await client.setRoomAlias('#neuer-alias:server', room.id); // Kanonischen Alias setzen (sichtbarer Hauptalias) await room.client.sendState( room.id, EventTypes.RoomCanonicalAlias, { 'alias': '#hauptalias:server', 'alt_aliases': ['#alternativ:server'], }, stateKey: '', ); // Alias auflösen → Raum-ID final resolved = await client.getRoomIdByAlias('#alias:server'); print(resolved.roomId); // !roomId:server // Alias löschen await client.deleteRoomAlias('#alter-alias:server'); // Alle Aliases eines Raums final aliases = (room.getState(EventTypes.RoomCanonicalAlias) ?.content['alt_aliases'] as List?) ?.cast() ?? []; final canonical = room.canonicalAlias; ``` --- ## Server ACL (IP-Sperren) ```dart // Server ACL: Welche Server dürfen am Raum teilnehmen? // Wichtig für öffentliche Räume um Spam-Server zu sperren. await room.client.sendState( room.id, 'm.room.server_acl', { 'allow': ['*'], // Alle erlauben 'deny': ['spam.example.org', '*.badserver.org'], 'allow_ip_literals': false, // IP-Adressen als Server-Namen verboten }, stateKey: '', ); // ACL lesen final acl = room.getState('m.room.server_acl')?.content; final denied = (acl?['deny'] as List?)?.cast() ?? []; ``` --- ## Raum-Audit / State inspizieren ```dart // Alle State-Events lesen final states = room.states; // Map> // Spezifischer State final nameEvent = room.getState(EventTypes.RoomName); final topicEvent = room.getState(EventTypes.RoomTopic); final memberEvent = room.getState(EventTypes.RoomMember, '@alice:server'); final encEvent = room.getState(EventTypes.Encryption); // Alle Mitglieder-States final memberStates = states[EventTypes.RoomMember] ?? {}; for (final entry in memberStates.entries) { final mxid = entry.key; // stateKey = MXID final membership = entry.value.content['membership']; print('$mxid: $membership'); } // Raum-History vom Server laden (Admin) final response = await client.request( RequestType.GET, '/rooms/${room.id}/state', ); // Event-Timeline abrufen (Admin) final timeline = await client.request( RequestType.GET, '/rooms/${room.id}/messages', query: {'limit': '100', 'dir': 'b'}, ); ``` --- ## Widgets / Bots verwalten ```dart // Widget hinzufügen (z.B. Jitsi, Etherpad) await room.client.sendState( room.id, 'm.widget', { 'type': 'jitsi', 'url': 'https://jitsi.example.org/#!roomId', 'name': 'Jitsi Call', 'data': {'domain': 'jitsi.example.org'}, 'waitForIframeLoad': true, 'creatorUserId': client.userID, 'id': 'jitsi-widget', }, stateKey: 'jitsi-widget', ); // Widget entfernen await room.client.sendState( room.id, 'm.widget', {}, // Leeres Content = entfernen stateKey: 'jitsi-widget', ); // Alle Widgets lesen final widgets = room.states['m.widget'] ?? {}; for (final widget in widgets.values) { print(widget.content['name']); print(widget.content['url']); } ``` --- ## Room Tombstone / Upgrade ```dart // Raum-Upgrade (Nachfolger-Raum erstellen) final newRoomId = await client.upgradeRoom( room.id, newVersion: '11', // Aktuelle empfohlene Version ); // SDK sendet m.room.tombstone in alten Raum // Und m.room.create mit predecessor in neuen Raum // Alle Mitglieder werden in neuen Raum eingeladen // Tombstone lesen final tombstone = room.getState(EventTypes.RoomTombstone); if (tombstone != null) { final replacementRoomId = tombstone.content['replacement_room'] as String; final serverMessage = tombstone.content['body'] as String; // → Zum neuen Raum navigieren } // Predecessor lesen (im neuen Raum) final create = room.getState(EventTypes.RoomCreate); final predecessor = create?.content['predecessor'] as Map?; if (predecessor != null) { final oldRoomId = predecessor['room_id'] as String; final lastEventId = predecessor['event_id'] as String; } ``` --- ## Reporting (Melden) ```dart // Event melden (z.B. Spam, illegaler Inhalt) await client.reportContent( room.id, event.eventId, reason: 'Spam/Belästigung', score: -100, // -100 = sehr schlimm, 0 = nicht schlimm ); // User melden (via MSC4151, wenn vom Server unterstützt) await client.httpClient.post( Uri.parse('${client.homeserver}/_matrix/client/v3/report/${Uri.encodeComponent("@user:server")}'), headers: { 'Authorization': 'Bearer ${client.accessToken}', 'Content-Type': 'application/json', }, body: jsonEncode({'reason': 'Belästigung'}), ); ```