using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Filters; using LibMatrix.Helpers; using LibMatrix.Homeservers; namespace MiniUtils.Services; public class AutoTombstoneFollowerService( AuthenticatedHomeserverGeneric hs, ILogger logger, MiniUtilsConfiguration config ) : IHostedService { private Task? _listenerTask; private readonly CancellationTokenSource _cts = new(); /// Triggered when the application host is ready to start the service. /// Indicates that the start process has been aborted. public Task StartAsync(CancellationToken cancellationToken) { if (!config.FollowTombstones) return Task.CompletedTask; _listenerTask = Run(_cts.Token); logger.LogInformation("Tombstone follower started (StartAsync)!"); return Task.CompletedTask; } private async Task? Run(CancellationToken cancellationToken) { logger.LogInformation("Starting Tombstone listener!"); var filter = await hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.miniutils.services.tombstone_follower", new SyncFilter() { AccountData = new SyncFilter.EventFilter(notTypes: ["*"], limit: 1), Presence = new SyncFilter.EventFilter(notTypes: ["*"]), Room = new SyncFilter.RoomFilter() { AccountData = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), State = new SyncFilter.RoomFilter.StateFilter(types: [RoomTombstoneEventContent.EventId]), Timeline = new SyncFilter.RoomFilter.StateFilter(types: [RoomTombstoneEventContent.EventId]), } }); var syncHelper = new SyncHelper(hs, logger) { FilterId = filter, UseMsc4222StateAfter = true }; syncHelper.SyncReceivedHandlers.Add(async sync => { logger.LogInformation("Sync received!"); var joinedRooms = await hs.GetJoinedRooms(); foreach (var roomResp in sync.Rooms?.Join ?? []) { if (roomResp.Value.StateAfter?.Events is null) continue; foreach (var @event in roomResp.Value.StateAfter.Events) { if (@event is not { Type: RoomTombstoneEventContent.EventId, StateKey: not null }) continue; var replacement = @event.ContentAs()!.ReplacementRoom; if (string.IsNullOrWhiteSpace(replacement)) { logger.LogError("[{}] Tombstone event with no replacement room!", roomResp.Key); continue; } var room = hs.GetRoom(roomResp.Key); if (joinedRooms.Any(x => x.RoomId == replacement)) { // logger.LogWarning("[{}] Replacement room {} is already joined!", roomResp.Key, replacement); continue; } await room.JoinAsync(reason: "Following tombstone", homeservers: [replacement.Split(':', 2)[1]]); await Task.Delay(1000, cancellationToken); joinedRooms = await hs.GetJoinedRooms(); } } }); await syncHelper.RunSyncLoopAsync(cancellationToken: _cts.Token); } /// Triggered when the application host is performing a graceful shutdown. /// Indicates that the shutdown process should no longer be graceful. public async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("Shutting down command listener!"); if (_listenerTask is null) { logger.LogError("Could not shut down command listener task because it was null!"); return; } await _cts.CancelAsync(); } }