Files
pyramid/docs/matrix-sdk/10-room-admin.md
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

411 lines
11 KiB
Markdown

# 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<String, dynamic>('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<String>() ?? [];
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<String>() ?? [];
```
---
## Raum-Audit / State inspizieren
```dart
// Alle State-Events lesen
final states = room.states;
// Map<EventType, Map<StateKey, StrippedStateEvent>>
// 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'}),
);
```