about summary refs log tree commit diff
path: root/LibMatrix/Helpers
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix/Helpers')
-rw-r--r--LibMatrix/Helpers/MatrixEventAttribute.cs7
-rw-r--r--LibMatrix/Helpers/MessageFormatter.cs9
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs259
-rw-r--r--LibMatrix/Helpers/SyncStateResolver.cs174
4 files changed, 254 insertions, 195 deletions
diff --git a/LibMatrix/Helpers/MatrixEventAttribute.cs b/LibMatrix/Helpers/MatrixEventAttribute.cs
deleted file mode 100644
index 7efc039..0000000
--- a/LibMatrix/Helpers/MatrixEventAttribute.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace LibMatrix.Helpers;
-
-[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
-public class MatrixEventAttribute : Attribute {
-    public string EventName { get; set; }
-    public bool Legacy { get; set; }
-}
diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs
index ae02afc..d252e85 100644
--- a/LibMatrix/Helpers/MessageFormatter.cs
+++ b/LibMatrix/Helpers/MessageFormatter.cs
@@ -13,8 +13,7 @@ public static class MessageFormatter {
 
     public static RoomMessageEventContent FormatException(string error, Exception e) {
         return new RoomMessageEventContent(body: $"{error}: {e.Message}", messageType: "m.text") {
-            FormattedBody = $"<font color=\"#FF0000\">{error}: <pre>{e.Message}</pre>" +
-                            $"</font>",
+            FormattedBody = $"<font color=\"#FF0000\">{error}: <pre>{e.Message}</pre></font>",
             Format = "org.matrix.custom.html"
         };
     }
@@ -36,4 +35,10 @@ public static class MessageFormatter {
     public static string HtmlFormatMention(string id, string? displayName = null) {
         return $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
     }
+
+#region Extension functions
+
+    public static RoomMessageEventContent ToMatrixMessage(this Exception e, string error) => FormatException(error, e);
+
+#endregion
 }
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 74972a1..06ae3fe 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -1,228 +1,115 @@
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Net.Http.Json;
-using System.Text.Json.Serialization;
 using ArcaneLibs.Extensions;
 using LibMatrix.Filters;
 using LibMatrix.Homeservers;
 using LibMatrix.Responses;
 using LibMatrix.Services;
+using Microsoft.Extensions.Logging;
 
 namespace LibMatrix.Helpers;
 
-public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) {
-    public async Task<SyncResult?> Sync(
-        string? since = null,
-        int? timeout = 30000,
-        string? setPresence = "online",
-        SyncFilter? filter = null,
-        CancellationToken? cancellationToken = null) {
-        var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}";
-        if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}";
-        if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}";
-        // else url += "&full_state=true";
-        Console.WriteLine("Calling: " + url);
-        try {
-            var req = await homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None);
+public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) {
+    public string? Since { get; set; }
+    public int Timeout { get; set; } = 30000;
+    public string? SetPresence { get; set; } = "online";
+    public SyncFilter? Filter { get; set; }
+    public bool FullState { get; set; } = false;
 
-#if DEBUG && false
-            try {
-                await homeserver._httpClient.PostAsync(
-                    "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName,
-                    new StreamContent(await req.Content.ReadAsStreamAsync()));
-            }
 
-            catch (Exception e) {
-                Console.WriteLine("[!!] Checking sync response failed: " + e);
-            }
-            var res = await req.Content.ReadFromJsonAsync<SyncResult>();
-            return res;
-#else
-            return await req.Content.ReadFromJsonAsync<SyncResult>();
-#endif
+    public async Task<SyncResponse?> SyncAsync(CancellationToken? cancellationToken = null) {
+        var url = $"/_matrix/client/v3/sync?timeout={Timeout}&set_presence={SetPresence}&full_state={(FullState ? "true" : "false")}";
+        if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}";
+        if (Filter is not null) url += $"&filter={Filter.ToJson(ignoreNull: true, indent: false)}";
+        // Console.WriteLine("Calling: " + url);
+        logger?.LogInformation("SyncHelper: Calling: {}", url);
+        try {
+            return await homeserver._httpClient.GetFromJsonAsync<SyncResponse>(url, cancellationToken: cancellationToken ?? CancellationToken.None);
         }
         catch (TaskCanceledException) {
             Console.WriteLine("Sync cancelled!");
+            logger?.LogWarning("Sync cancelled due to TaskCanceledException!");
         }
         catch (Exception e) {
             Console.WriteLine(e);
+            logger?.LogError(e, "Failed to sync!\n{}", e.ToString());
         }
 
         return null;
     }
 
-    [SuppressMessage("ReSharper", "FunctionNeverReturns")]
-    public async Task RunSyncLoop(
-        bool skipInitialSyncEvents = true,
-        string? since = null,
-        int? timeout = 30000,
-        string? setPresence = "online",
-        SyncFilter? filter = null,
-        CancellationToken? cancellationToken = null
-    ) {
-        // await Task.WhenAll((await storageService.CacheStorageProvider.GetAllKeysAsync())
-        //     .Where(x => x.StartsWith("sync"))
-        //     .ToList()
-        //     .Select(x => storageService.CacheStorageProvider.DeleteObjectAsync(x)));
-        var nextBatch = since;
-        while (cancellationToken is null || !cancellationToken.Value.IsCancellationRequested) {
-            var sync = await Sync(since: nextBatch, timeout: timeout, setPresence: setPresence, filter: filter,
-                cancellationToken: cancellationToken);
-            nextBatch = sync?.NextBatch ?? nextBatch;
+    public async IAsyncEnumerable<SyncResponse> EnumerateSyncAsync(CancellationToken? cancellationToken = null) {
+        while(!cancellationToken?.IsCancellationRequested ?? true) {
+            var sync = await SyncAsync(cancellationToken);
             if (sync is null) continue;
-            Console.WriteLine($"Got sync, next batch: {nextBatch}!");
-
-            if (sync.Rooms is { Invite.Count: > 0 }) {
-                foreach (var roomInvite in sync.Rooms.Invite) {
-                    var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList();
-                    await Task.WhenAll(tasks);
-                }
-            }
-
-            if (sync.AccountData is { Events: { Count: > 0 } }) {
-                foreach (var accountDataEvent in sync.AccountData.Events) {
-                    var tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList();
-                    await Task.WhenAll(tasks);
-                }
-            }
-
-            // Things that are skipped on the first sync
-            if (skipInitialSyncEvents) {
-                skipInitialSyncEvents = false;
-                continue;
-            }
-
-            if (sync.Rooms is { Join.Count: > 0 }) {
-                foreach (var updatedRoom in sync.Rooms.Join) {
-                    if(updatedRoom.Value.Timeline is null) continue;
-                    foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) {
-                        stateEventResponse.RoomId = updatedRoom.Key;
-                        var tasks = TimelineEventHandlers.Select(x => {
-                            try {
-                                return x(stateEventResponse);
-                            }
-                            catch (Exception e) {
-                                Console.WriteLine(e);
-                                return Task.CompletedTask;
-                            }
-                        }).ToList();
-                        await Task.WhenAll(tasks);
-                    }
-                }
-            }
+            Since = sync.NextBatch ?? Since;
+            yield return sync;
         }
     }
 
-    /// <summary>
-    /// Event fired when a room invite is received
-    /// </summary>
-    public List<Func<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>, Task>>
-        InviteReceivedHandlers { get; } = new();
-
-    public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new();
-    public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new();
-}
-
-public class SyncResult {
-    [JsonPropertyName("next_batch")]
-    public string NextBatch { get; set; }
-
-    [JsonPropertyName("account_data")]
-    public EventList? AccountData { get; set; }
-
-    [JsonPropertyName("presence")]
-    public PresenceDataStructure? Presence { get; set; }
-
-    [JsonPropertyName("device_one_time_keys_count")]
-    public Dictionary<string, int> DeviceOneTimeKeysCount { get; set; }
-
-    [JsonPropertyName("rooms")]
-    public RoomsDataStructure? Rooms { get; set; }
-
-    [JsonPropertyName("to_device")]
-    public EventList? ToDevice { get; set; }
-
-    [JsonPropertyName("device_lists")]
-    public DeviceListsDataStructure? DeviceLists { get; set; }
-
-    public class DeviceListsDataStructure {
-        [JsonPropertyName("changed")]
-        public List<string>? Changed { get; set; }
-
-        [JsonPropertyName("left")]
-        public List<string>? Left { get; set; }
-    }
-
-    // supporting classes
-    public class PresenceDataStructure {
-        [JsonPropertyName("events")]
-        public List<StateEventResponse> Events { get; set; }
+    public async Task RunSyncLoopAsync(bool skipInitialSyncEvents = true, CancellationToken? cancellationToken = null) {
+        var sw = Stopwatch.StartNew();
+        await foreach (var sync in EnumerateSyncAsync(cancellationToken)) {
+            logger?.LogInformation("Got sync response: {} bytes, {} elapsed", sync?.ToJson(ignoreNull: true, indent: false).Length ?? -1, sw.Elapsed);
+            await RunSyncLoopCallbacksAsync(sync, Since is null && skipInitialSyncEvents);
+        }
     }
 
-    public class RoomsDataStructure {
-        [JsonPropertyName("join")]
-        public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; }
-
-        [JsonPropertyName("invite")]
-        public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; }
-
-        public class JoinedRoomDataStructure {
-            [JsonPropertyName("timeline")]
-            public TimelineDataStructure? Timeline { get; set; }
-
-            [JsonPropertyName("state")]
-            public EventList State { get; set; }
-
-            [JsonPropertyName("account_data")]
-            public EventList AccountData { get; set; }
-
-            [JsonPropertyName("ephemeral")]
-            public EventList Ephemeral { get; set; }
-
-            [JsonPropertyName("unread_notifications")]
-            public UnreadNotificationsDataStructure UnreadNotifications { get; set; }
-
-            [JsonPropertyName("summary")]
-            public SummaryDataStructure Summary { get; set; }
+    private async Task RunSyncLoopCallbacksAsync(SyncResponse syncResponse, bool isInitialSync) {
 
-            public class TimelineDataStructure {
-                [JsonPropertyName("events")]
-                public List<StateEventResponse> Events { get; set; }
+        var tasks = SyncReceivedHandlers.Select(x => x(syncResponse)).ToList();
+        await Task.WhenAll(tasks);
 
-                [JsonPropertyName("prev_batch")]
-                public string PrevBatch { get; set; }
-
-                [JsonPropertyName("limited")]
-                public bool Limited { get; set; }
+        if (syncResponse.AccountData is { Events: { Count: > 0 } }) {
+            foreach (var accountDataEvent in syncResponse.AccountData.Events) {
+                tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList();
+                await Task.WhenAll(tasks);
             }
+        }
 
-            public class UnreadNotificationsDataStructure {
-                [JsonPropertyName("notification_count")]
-                public int NotificationCount { get; set; }
+        await RunSyncLoopRoomCallbacksAsync(syncResponse, isInitialSync);
+    }
 
-                [JsonPropertyName("highlight_count")]
-                public int HighlightCount { get; set; }
+    private async Task RunSyncLoopRoomCallbacksAsync(SyncResponse syncResponse, bool isInitialSync) {
+        if (syncResponse.Rooms is { Invite.Count: > 0 }) {
+            foreach (var roomInvite in syncResponse.Rooms.Invite) {
+                var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList();
+                await Task.WhenAll(tasks);
             }
+        }
 
-            public class SummaryDataStructure {
-                [JsonPropertyName("m.heroes")]
-                public List<string> Heroes { get; set; }
-
-                [JsonPropertyName("m.invited_member_count")]
-                public int InvitedMemberCount { get; set; }
+        if (isInitialSync) return;
 
-                [JsonPropertyName("m.joined_member_count")]
-                public int JoinedMemberCount { get; set; }
+        if (syncResponse.Rooms is { Join.Count: > 0 }) {
+            foreach (var updatedRoom in syncResponse.Rooms.Join) {
+                if (updatedRoom.Value.Timeline is null) continue;
+                foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) {
+                    stateEventResponse.RoomId = updatedRoom.Key;
+                    var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList();
+                    await Task.WhenAll(tasks);
+                }
             }
         }
-
-        public class InvitedRoomDataStructure {
-            [JsonPropertyName("invite_state")]
-            public EventList InviteState { get; set; }
-        }
     }
-}
 
-public class EventList {
-    [JsonPropertyName("events")]
-    public List<StateEventResponse> Events { get; set; }
+    /// <summary>
+    /// Event fired when a sync response is received
+    /// </summary>
+    public List<Func<SyncResponse, Task>> SyncReceivedHandlers { get; } = new();
+
+    /// <summary>
+    /// Event fired when a room invite is received
+    /// </summary>
+    public List<Func<KeyValuePair<string, SyncResponse.RoomsDataStructure.InvitedRoomDataStructure>, Task>> InviteReceivedHandlers { get; } = new();
+
+    /// <summary>
+    /// Event fired when a timeline event is received
+    /// </summary>
+    public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new();
+
+    /// <summary>
+    /// Event fired when an account data event is received
+    /// </summary>
+    public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new();
 }
diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs
new file mode 100644
index 0000000..0070d60
--- /dev/null
+++ b/LibMatrix/Helpers/SyncStateResolver.cs
@@ -0,0 +1,174 @@
+using LibMatrix.Extensions;
+using LibMatrix.Filters;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using Microsoft.Extensions.Logging;
+
+namespace LibMatrix.Helpers;
+
+public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) {
+    public string? Since { get; set; }
+    public int Timeout { get; set; } = 30000;
+    public string? SetPresence { get; set; } = "online";
+    public SyncFilter? Filter { get; set; }
+    public bool FullState { get; set; } = false;
+
+    public SyncResponse? MergedState { get; set; } = null!;
+
+    private SyncHelper _syncHelper = new SyncHelper(homeserver, logger);
+
+    public async Task<(SyncResponse next, SyncResponse merged)> ContinueAsync(CancellationToken? cancellationToken = null) {
+        // copy properties
+        _syncHelper.Since = Since;
+        _syncHelper.Timeout = Timeout;
+        _syncHelper.SetPresence = SetPresence;
+        _syncHelper.Filter = Filter;
+        _syncHelper.FullState = FullState;
+        // run sync
+        var sync = await _syncHelper.SyncAsync(cancellationToken);
+        if (sync is null) return await ContinueAsync(cancellationToken);
+        if (MergedState is null) MergedState = sync;
+        else MergedState = MergeSyncs(MergedState, sync);
+        Since = sync.NextBatch;
+        return (sync, MergedState);
+    }
+
+    private SyncResponse MergeSyncs(SyncResponse oldState, SyncResponse newState) {
+        oldState.NextBatch = newState.NextBatch ?? oldState.NextBatch;
+
+        oldState.AccountData ??= new();
+        oldState.AccountData.Events ??= new();
+        if (newState.AccountData?.Events is not null)
+            oldState.AccountData.Events.MergeStateEventLists(newState.AccountData?.Events ?? new());
+
+        oldState.Presence ??= new();
+        if (newState.Presence?.Events is not null)
+            oldState.Presence.Events.MergeStateEventLists(newState.Presence?.Events ?? new());
+
+        oldState.DeviceOneTimeKeysCount ??= new();
+        if (newState.DeviceOneTimeKeysCount is not null)
+            foreach (var (key, value) in newState.DeviceOneTimeKeysCount) {
+                oldState.DeviceOneTimeKeysCount[key] = value;
+            }
+
+        oldState.Rooms ??= new();
+        if (newState.Rooms is not null)
+            oldState.Rooms = MergeRoomsDataStructure(oldState.Rooms, newState.Rooms);
+
+        oldState.ToDevice ??= new();
+        oldState.ToDevice.Events ??= new();
+        if (newState.ToDevice?.Events is not null)
+            oldState.ToDevice.Events.MergeStateEventLists(newState.ToDevice?.Events ?? new());
+
+        oldState.DeviceLists ??= new();
+        if (newState.DeviceLists?.Changed is not null)
+            foreach (var s in oldState.DeviceLists.Changed!) {
+                oldState.DeviceLists.Changed.Add(s);
+            }
+        if (newState.DeviceLists?.Left is not null)
+            foreach (var s in oldState.DeviceLists.Left!) {
+                oldState.DeviceLists.Left.Add(s);
+            }
+
+
+        return oldState;
+    }
+
+#region Merge rooms
+
+    private SyncResponse.RoomsDataStructure MergeRoomsDataStructure(SyncResponse.RoomsDataStructure oldState, SyncResponse.RoomsDataStructure newState) {
+        oldState.Join ??= new();
+        foreach (var (key, value) in newState.Join ?? new()) {
+            if (!oldState.Join.ContainsKey(key)) oldState.Join[key] = value;
+            else oldState.Join[key] = MergeJoinedRoomDataStructure(oldState.Join[key], value);
+        }
+
+        oldState.Invite ??= new();
+        foreach (var (key, value) in newState.Invite ?? new()) {
+            if (!oldState.Invite.ContainsKey(key)) oldState.Invite[key] = value;
+            else oldState.Invite[key] = MergeInvitedRoomDataStructure(oldState.Invite[key], value);
+        }
+
+        oldState.Leave ??= new();
+        foreach (var (key, value) in newState.Leave ?? new()) {
+            if (!oldState.Leave.ContainsKey(key)) oldState.Leave[key] = value;
+            else oldState.Leave[key] = MergeLeftRoomDataStructure(oldState.Leave[key], value);
+            if (oldState.Invite.ContainsKey(key)) oldState.Invite.Remove(key);
+            if (oldState.Join.ContainsKey(key)) oldState.Join.Remove(key);
+        }
+
+        return oldState;
+    }
+
+    private SyncResponse.RoomsDataStructure.LeftRoomDataStructure MergeLeftRoomDataStructure(SyncResponse.RoomsDataStructure.LeftRoomDataStructure oldData,
+        SyncResponse.RoomsDataStructure.LeftRoomDataStructure newData) {
+        oldData.AccountData ??= new();
+        oldData.AccountData.Events ??= new();
+        oldData.Timeline ??= new();
+        oldData.Timeline.Events ??= new();
+        oldData.State ??= new();
+        oldData.State.Events ??= new();
+
+        if (newData.AccountData?.Events is not null)
+            oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new());
+
+        if (newData.Timeline?.Events is not null)
+            oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new());
+        oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited;
+        oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch;
+
+        if (newData.State?.Events is not null)
+            oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new());
+
+        return oldData;
+    }
+
+    private SyncResponse.RoomsDataStructure.InvitedRoomDataStructure MergeInvitedRoomDataStructure(SyncResponse.RoomsDataStructure.InvitedRoomDataStructure oldData,
+        SyncResponse.RoomsDataStructure.InvitedRoomDataStructure newData) {
+        oldData.InviteState ??= new();
+        oldData.InviteState.Events ??= new();
+        if (newData.InviteState?.Events is not null)
+            oldData.InviteState.Events.MergeStateEventLists(newData.InviteState?.Events ?? new());
+
+        return oldData;
+    }
+
+    private SyncResponse.RoomsDataStructure.JoinedRoomDataStructure MergeJoinedRoomDataStructure(SyncResponse.RoomsDataStructure.JoinedRoomDataStructure oldData,
+        SyncResponse.RoomsDataStructure.JoinedRoomDataStructure newData) {
+        oldData.AccountData ??= new();
+        oldData.AccountData.Events ??= new();
+        oldData.Timeline ??= new();
+        oldData.Timeline.Events ??= new();
+        oldData.State ??= new();
+        oldData.State.Events ??= new();
+        oldData.Ephemeral ??= new();
+        oldData.Ephemeral.Events ??= new();
+
+        if (newData.AccountData?.Events is not null)
+            oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new());
+
+        if (newData.Timeline?.Events is not null)
+            oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new());
+        oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited;
+        oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch;
+
+        if (newData.State?.Events is not null)
+            oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new());
+
+        if (newData.Ephemeral?.Events is not null)
+            oldData.Ephemeral.Events.MergeStateEventLists(newData.Ephemeral?.Events ?? new());
+
+        oldData.UnreadNotifications ??= new();
+        oldData.UnreadNotifications.HighlightCount = newData.UnreadNotifications?.HighlightCount ?? oldData.UnreadNotifications.HighlightCount;
+        oldData.UnreadNotifications.NotificationCount = newData.UnreadNotifications?.NotificationCount ?? oldData.UnreadNotifications.NotificationCount;
+
+        oldData.Summary ??= new();
+        oldData.Summary.Heroes = newData.Summary?.Heroes ?? oldData.Summary.Heroes;
+        oldData.Summary.JoinedMemberCount = newData.Summary?.JoinedMemberCount ?? oldData.Summary.JoinedMemberCount;
+        oldData.Summary.InvitedMemberCount = newData.Summary?.InvitedMemberCount ?? oldData.Summary.InvitedMemberCount;
+
+        return oldData;
+    }
+
+#endregion
+}