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
+}
|