diff options
Diffstat (limited to 'LibMatrix/Helpers')
-rw-r--r-- | LibMatrix/Helpers/MatrixEventAttribute.cs | 7 | ||||
-rw-r--r-- | LibMatrix/Helpers/MessageFormatter.cs | 9 | ||||
-rw-r--r-- | LibMatrix/Helpers/SyncHelper.cs | 259 | ||||
-rw-r--r-- | LibMatrix/Helpers/SyncStateResolver.cs | 174 |
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 +} |