Files
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

11 KiB

10 — Raum-Administration & Moderation


Inhaltsverzeichnis


Power Levels vollständig

// 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

// 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

// 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},
);
# 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

// 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

// 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)

// 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

// 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

// 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

// 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)

// 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'}),
);