diff --git a/LibMatrix/EventTypes/Common/MjolnirShortcodeEventData.cs b/LibMatrix/EventTypes/Common/MjolnirShortcodeEventContent.cs
index 9067351..9067351 100644
--- a/LibMatrix/EventTypes/Common/MjolnirShortcodeEventData.cs
+++ b/LibMatrix/EventTypes/Common/MjolnirShortcodeEventContent.cs
diff --git a/LibMatrix/EventTypes/Common/RoomEmotesEventData.cs b/LibMatrix/EventTypes/Common/RoomEmotesEventContent.cs
index abf936c..abf936c 100644
--- a/LibMatrix/EventTypes/Common/RoomEmotesEventData.cs
+++ b/LibMatrix/EventTypes/Common/RoomEmotesEventContent.cs
diff --git a/LibMatrix/Helpers/MatrixEventAttribute.cs b/LibMatrix/EventTypes/MatrixEventAttribute.cs
index 7efc039..92334d0 100644
--- a/LibMatrix/Helpers/MatrixEventAttribute.cs
+++ b/LibMatrix/EventTypes/MatrixEventAttribute.cs
@@ -1,4 +1,4 @@
-namespace LibMatrix.Helpers;
+namespace LibMatrix.EventTypes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class MatrixEventAttribute : Attribute {
diff --git a/LibMatrix/EventTypes/Spec/State/PresenceStateEventData.cs b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs
index b12da5b..b12da5b 100644
--- a/LibMatrix/EventTypes/Spec/State/PresenceStateEventData.cs
+++ b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomTypingEventData.cs b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
index 01cfacf..01cfacf 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomTypingEventData.cs
+++ b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs
index f8ee58b..f8ee58b 100644
--- a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs
+++ b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/PolicyRuleStateEventData.cs b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index fde02c1..fde02c1 100644
--- a/LibMatrix/EventTypes/Spec/State/PolicyRuleStateEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/ProfileResponseEventData.cs b/LibMatrix/EventTypes/Spec/State/ProfileResponseEventContent.cs
index 893fce1..893fce1 100644
--- a/LibMatrix/EventTypes/Spec/State/ProfileResponseEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/ProfileResponseEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomAliasEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs
index 5b0e914..5b0e914 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomAliasEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomAvatarEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs
index 601d014..601d014 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomAvatarEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/CanonicalAliasEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
index 71f3d0d..046222e 100644
--- a/LibMatrix/EventTypes/Spec/State/CanonicalAliasEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
namespace LibMatrix.EventTypes.Spec.State;
[MatrixEvent(EventName = "m.room.canonical_alias")]
-public class CanonicalAliasEventContent : EventContent {
+public class RoomCanonicalAliasEventContent : EventContent {
[JsonPropertyName("alias")]
public string? Alias { get; set; }
[JsonPropertyName("alt_aliases")]
diff --git a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
index c5bf14e..c5bf14e 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomEncryptionEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs
index 6ffa4c5..6ffa4c5 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomEncryptionEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/GuestAccessEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs
index af1b2ce..2bb4d36 100644
--- a/LibMatrix/EventTypes/Spec/State/GuestAccessEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
namespace LibMatrix.EventTypes.Spec.State;
[MatrixEvent(EventName = "m.room.guest_access")]
-public class GuestAccessEventContent : EventContent {
+public class RoomGuestAccessEventContent : EventContent {
[JsonPropertyName("guest_access")]
public string GuestAccess { get; set; }
diff --git a/LibMatrix/EventTypes/Spec/State/HistoryVisibilityEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
index b57ade5..a32fed2 100644
--- a/LibMatrix/EventTypes/Spec/State/HistoryVisibilityEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
namespace LibMatrix.EventTypes.Spec.State;
[MatrixEvent(EventName = "m.room.history_visibility")]
-public class HistoryVisibilityEventContent : EventContent {
+public class RoomHistoryVisibilityEventContent : EventContent {
[JsonPropertyName("history_visibility")]
public string HistoryVisibility { get; set; }
}
diff --git a/LibMatrix/EventTypes/Spec/State/JoinRulesEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs
index 0098bef..2c2a91b 100644
--- a/LibMatrix/EventTypes/Spec/State/JoinRulesEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
namespace LibMatrix.EventTypes.Spec.State;
[MatrixEvent(EventName = "m.room.join_rules")]
-public class JoinRulesEventContent : EventContent {
+public class RoomJoinRulesEventContent : EventContent {
private static string Public = "public";
private static string Invite = "invite";
private static string Knock = "knock";
diff --git a/LibMatrix/EventTypes/Spec/State/RoomMemberEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs
index da158f1..52cb293 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomMemberEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs
@@ -13,7 +13,7 @@ public class RoomMemberEventContent : EventContent {
public string Membership { get; set; } = null!;
[JsonPropertyName("displayname")]
- public string? Displayname { get; set; }
+ public string? DisplayName { get; set; }
[JsonPropertyName("is_direct")]
public bool? IsDirect { get; set; }
diff --git a/LibMatrix/EventTypes/Spec/State/RoomNameEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
index 7cb881a..7cb881a 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomNameEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomPinnedEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs
index eb02cc7..eb02cc7 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomPinnedEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index 2ae9593..2ae9593 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/ServerACLEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs
index f18fe43..5c5627c 100644
--- a/LibMatrix/EventTypes/Spec/State/ServerACLEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs
@@ -5,7 +5,7 @@ using LibMatrix.Interfaces;
namespace LibMatrix.EventTypes.Spec.State;
[MatrixEvent(EventName = "m.room.server_acl")]
-public class ServerACLEventContent : EventContent {
+public class RoomServerACLEventContent : EventContent {
[JsonPropertyName("allow")]
public List<string> Allow { get; set; } // = null!;
diff --git a/LibMatrix/EventTypes/Spec/State/RoomTopicEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs
index 52c7e42..52c7e42 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomTopicEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs b/LibMatrix/EventTypes/Spec/State/Space/SpaceChildEventContent.cs
index 0a897dc..0a897dc 100644
--- a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/Space/SpaceChildEventContent.cs
diff --git a/LibMatrix/EventTypes/Spec/State/SpaceParentEventData.cs b/LibMatrix/EventTypes/Spec/State/Space/SpaceParentEventContent.cs
index 0ffa193..0ffa193 100644
--- a/LibMatrix/EventTypes/Spec/State/SpaceParentEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/Space/SpaceParentEventContent.cs
diff --git a/LibMatrix/EventTypes/UnknownStateEventData.cs b/LibMatrix/EventTypes/UnknownStateEventContent.cs
index 9a276c8..9a276c8 100644
--- a/LibMatrix/EventTypes/UnknownStateEventData.cs
+++ b/LibMatrix/EventTypes/UnknownStateEventContent.cs
diff --git a/LibMatrix/Extensions/EnumerableExtensions.cs b/LibMatrix/Extensions/EnumerableExtensions.cs
new file mode 100644
index 0000000..d9619b7
--- /dev/null
+++ b/LibMatrix/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,28 @@
+namespace LibMatrix.Extensions;
+
+public static class EnumerableExtensions {
+ public static void MergeStateEventLists(this List<StateEvent> oldState, List<StateEvent> newState) {
+ foreach (var stateEvent in newState) {
+ var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey);
+ if (old is null) {
+ oldState.Add(stateEvent);
+ continue;
+ }
+ oldState.Remove(old);
+ oldState.Add(stateEvent);
+ }
+ }
+
+ public static void MergeStateEventLists(this List<StateEventResponse> oldState, List<StateEventResponse> newState) {
+ foreach (var stateEvent in newState) {
+ var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey);
+ if (old is null) {
+ oldState.Add(stateEvent);
+ continue;
+ }
+ oldState.Remove(old);
+ oldState.Add(stateEvent);
+ }
+ }
+
+}
diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs
index a5eb40f..2fe99b6 100644
--- a/LibMatrix/Extensions/HttpClientExtensions.cs
+++ b/LibMatrix/Extensions/HttpClientExtensions.cs
@@ -68,7 +68,16 @@ public class MatrixHttpClient : HttpClient {
var response = await SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
- return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken);
+#if DEBUG && false // This is only used for testing, so it's disabled by default
+ try {
+ await PostAsync("http://localhost:5116/validate/" + typeof(T).AssemblyQualifiedName, new StreamContent(responseStream), cancellationToken);
+ }
+ catch (Exception e) {
+ Console.WriteLine("[!!] Checking sync response failed: " + e);
+ }
+#endif
+ return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken) ??
+ throw new InvalidOperationException("Failed to deserialize response");
}
// GetStreamAsync
@@ -80,7 +89,8 @@ public class MatrixHttpClient : HttpClient {
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
- public new async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
+ public new async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
+ CancellationToken cancellationToken = default) {
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType()), Encoding.UTF8, "application/json");
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
+}
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index f70dd39..d5b0a77 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -12,19 +12,35 @@ using LibMatrix.Services;
namespace LibMatrix.Homeservers;
-public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
- public AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : base(baseUrl) {
- AccessToken = accessToken.Trim();
- SyncHelper = new SyncHelper(this);
-
- _httpClient.Timeout = TimeSpan.FromMinutes(15);
- _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
+public class AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : RemoteHomeServer(baseUrl) {
+ public static async Task<T> Create<T>(string baseUrl, string accessToken) where T : AuthenticatedHomeserverGeneric {
+ var instance = Activator.CreateInstance(typeof(T), baseUrl, accessToken) as T
+ ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}");
+ instance._httpClient = new() {
+ BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl)
+ ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+ Timeout = TimeSpan.FromMinutes(15),
+ DefaultRequestHeaders = {
+ Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
+ }
+ };
+ instance.WhoAmI = await instance._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami");
+ return instance;
}
- public virtual SyncHelper SyncHelper { get; init; }
- private WhoAmIResponse? _whoAmI;
+ // Activator.CreateInstance(baseUrl, accessToken) {
+ // _httpClient = new() {
+ // BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl)
+ // ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+ // Timeout = TimeSpan.FromMinutes(15),
+ // DefaultRequestHeaders = {
+ // Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
+ // }
+ // }
+ // };
+
- public WhoAmIResponse? WhoAmI => _whoAmI ??= _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami").Result;
+ public WhoAmIResponse? WhoAmI { get; set; }
public string UserId => WhoAmI.UserId;
// public virtual async Task<WhoAmIResponse> WhoAmI() {
@@ -33,9 +49,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
// return _whoAmI;
// }
- public virtual string AccessToken { get; set; }
+ public string AccessToken { get; set; } = accessToken;
- public virtual GenericRoom GetRoom(string roomId) {
+ public GenericRoom GetRoom(string roomId) {
if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId));
return new GenericRoom(this, roomId);
}
@@ -112,7 +128,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
#region Account Data
- public virtual async Task<T> GetAccountData<T>(string key) {
+ public virtual async Task<T> GetAccountDataAsync<T>(string key) {
// var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}");
// if (!res.IsSuccessStatusCode) {
// Console.WriteLine($"Failed to get account data: {await res.Content.ReadAsStringAsync()}");
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index d10c837..798349a 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -1,3 +1,4 @@
+using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -10,13 +11,18 @@ using LibMatrix.Services;
namespace LibMatrix.Homeservers;
public class RemoteHomeServer(string baseUrl) {
+ public static async Task<RemoteHomeServer> Create(string baseUrl) =>
+ new(baseUrl) {
+ _httpClient = new() {
+ BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl)
+ ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+ Timeout = TimeSpan.FromSeconds(120)
+ }
+ };
private Dictionary<string, object> _profileCache { get; set; } = new();
- public string BaseUrl { get; } = baseUrl.Trim();
- public MatrixHttpClient _httpClient { get; set; } = new() {
- BaseAddress = new Uri(new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl).Result ?? throw new InvalidOperationException("Failed to resolve homeserver")),
- Timeout = TimeSpan.FromSeconds(120)
- };
+ public string BaseUrl { get; } = baseUrl;
+ public MatrixHttpClient _httpClient { get; set; }
public async Task<ProfileResponseEventContent> GetProfileAsync(string mxid) {
if (mxid is null) throw new ArgumentNullException(nameof(mxid));
diff --git a/LibMatrix/Interfaces/IStateEventType.cs b/LibMatrix/Interfaces/EventContent.cs
index b187970..b21cfc7 100644
--- a/LibMatrix/Interfaces/IStateEventType.cs
+++ b/LibMatrix/Interfaces/EventContent.cs
@@ -9,7 +9,7 @@ public abstract class EventContent {
[JsonPropertyName("m.new_content")]
public EventContent? NewContent { get; set; }
- public abstract class MessageRelatesTo {
+ public class MessageRelatesTo {
[JsonPropertyName("m.in_reply_to")]
public EventInReplyTo? InReplyTo { get; set; }
@@ -18,6 +18,9 @@ public abstract class EventContent {
public abstract class EventInReplyTo {
[JsonPropertyName("event_id")]
public string EventId { get; set; }
+
+ [JsonPropertyName("rel_type")]
+ public string RelType { get; set; }
}
}
}
diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
index 511b3da..1ad590f 100644
--- a/LibMatrix/Responses/CreateRoomRequest.cs
+++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -2,6 +2,7 @@ using System.Reflection;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
+using LibMatrix.EventTypes;
using LibMatrix.EventTypes.Spec.State;
using LibMatrix.Helpers;
using LibMatrix.Homeservers;
diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
index eb53c0a..07b1601 100644
--- a/LibMatrix/Responses/LoginResponse.cs
+++ b/LibMatrix/Responses/LoginResponse.cs
@@ -23,7 +23,7 @@ public class LoginResponse {
public string UserId { get; set; } = null!;
public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) {
- return new AuthenticatedHomeserverGeneric(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken);
+ return await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken);
}
}
public class LoginRequest {
diff --git a/LibMatrix/Responses/StateEventResponse.cs b/LibMatrix/Responses/StateEventResponse.cs
deleted file mode 100644
index 7ca6bab..0000000
--- a/LibMatrix/Responses/StateEventResponse.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
-
-namespace LibMatrix.Responses;
-
-public class StateEventResponse : StateEvent {
- [JsonPropertyName("origin_server_ts")]
- public ulong OriginServerTs { get; set; }
-
- [JsonPropertyName("room_id")]
- public string RoomId { get; set; }
-
- [JsonPropertyName("sender")]
- public string Sender { get; set; }
-
- [JsonPropertyName("unsigned")]
- public UnsignedData? Unsigned { get; set; }
-
- [JsonPropertyName("event_id")]
- public string EventId { get; set; }
-
- [JsonPropertyName("user_id")]
- public string UserId { get; set; }
-
- [JsonPropertyName("replaces_state")]
- public new string ReplacesState { get; set; }
-
- public class UnsignedData {
- [JsonPropertyName("age")]
- public ulong? Age { get; set; }
-
- [JsonPropertyName("redacted_because")]
- public object? RedactedBecause { get; set; }
-
- [JsonPropertyName("transaction_id")]
- public string? TransactionId { get; set; }
-
- [JsonPropertyName("replaces_state")]
- public string? ReplacesState { get; set; }
-
- [JsonPropertyName("prev_sender")]
- public string? PrevSender { get; set; }
-
- [JsonPropertyName("prev_content")]
- public JsonObject? PrevContent { get; set; }
- }
-}
-
-public class ChunkedStateEventResponse {
- [JsonPropertyName("chunk")]
- public List<StateEventResponse>? Chunk { get; set; }
-}
diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs
new file mode 100644
index 0000000..39cb38f
--- /dev/null
+++ b/LibMatrix/Responses/SyncResponse.cs
@@ -0,0 +1,118 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Helpers;
+
+namespace LibMatrix.Responses;
+
+public class SyncResponse {
+ [JsonPropertyName("next_batch")]
+ public string NextBatch { get; set; } = null!;
+
+ [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; } = null!;
+
+ [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; } = new();
+ }
+
+ public class RoomsDataStructure {
+ [JsonPropertyName("join")]
+ public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; }
+
+ [JsonPropertyName("invite")]
+ public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; }
+
+ [JsonPropertyName("leave")]
+ public Dictionary<string, LeftRoomDataStructure>? Leave { get; set; }
+
+ public class LeftRoomDataStructure {
+ [JsonPropertyName("account_data")]
+ public EventList AccountData { get; set; }
+
+ [JsonPropertyName("timeline")]
+ public JoinedRoomDataStructure.TimelineDataStructure? Timeline { get; set; }
+
+ [JsonPropertyName("state")]
+ public EventList State { 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; }
+
+ public class TimelineDataStructure {
+ [JsonPropertyName("events")]
+ public List<StateEventResponse>? Events { get; set; }
+
+ [JsonPropertyName("prev_batch")]
+ public string? PrevBatch { get; set; }
+
+ [JsonPropertyName("limited")]
+ public bool? Limited { get; set; }
+ }
+
+ public class UnreadNotificationsDataStructure {
+ [JsonPropertyName("notification_count")]
+ public int NotificationCount { get; set; }
+
+ [JsonPropertyName("highlight_count")]
+ public int HighlightCount { get; set; }
+ }
+
+ public class SummaryDataStructure {
+ [JsonPropertyName("m.heroes")]
+ public List<string> Heroes { get; set; }
+
+ [JsonPropertyName("m.invited_member_count")]
+ public int InvitedMemberCount { get; set; }
+
+ [JsonPropertyName("m.joined_member_count")]
+ public int JoinedMemberCount { get; set; }
+ }
+ }
+
+ public class InvitedRoomDataStructure {
+ [JsonPropertyName("invite_state")]
+ public EventList? InviteState { get; set; }
+ }
+ }
+}
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 78a0873..75cb5f3 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -118,16 +118,16 @@ public class GenericRoom {
#region Utility shortcuts
- public async Task<EventIdResponse> SendMessageEventAsync(RoomMessageEventContent content) =>
+ public async Task<EventIdResponse?> SendMessageEventAsync(RoomMessageEventContent content) =>
await SendTimelineEventAsync("m.room.message", content);
- public async Task<List<string>> GetAliasesAsync() {
+ public async Task<List<string>?> GetAliasesAsync() {
var res = await GetStateAsync<RoomAliasEventContent>("m.room.aliases");
return res.Aliases;
}
- public async Task<CanonicalAliasEventContent?> GetCanonicalAliasAsync() =>
- await GetStateAsync<CanonicalAliasEventContent>("m.room.canonical_alias");
+ public async Task<RoomCanonicalAliasEventContent?> GetCanonicalAliasAsync() =>
+ await GetStateAsync<RoomCanonicalAliasEventContent>("m.room.canonical_alias");
public async Task<RoomTopicEventContent?> GetTopicAsync() =>
await GetStateAsync<RoomTopicEventContent>("m.room.topic");
@@ -135,16 +135,16 @@ public class GenericRoom {
public async Task<RoomAvatarEventContent?> GetAvatarUrlAsync() =>
await GetStateAsync<RoomAvatarEventContent>("m.room.avatar");
- public async Task<JoinRulesEventContent> GetJoinRuleAsync() =>
- await GetStateAsync<JoinRulesEventContent>("m.room.join_rules");
+ public async Task<RoomJoinRulesEventContent?> GetJoinRuleAsync() =>
+ await GetStateAsync<RoomJoinRulesEventContent>("m.room.join_rules");
- public async Task<HistoryVisibilityEventContent?> GetHistoryVisibilityAsync() =>
- await GetStateAsync<HistoryVisibilityEventContent>("m.room.history_visibility");
+ public async Task<RoomHistoryVisibilityEventContent?> GetHistoryVisibilityAsync() =>
+ await GetStateAsync<RoomHistoryVisibilityEventContent?>("m.room.history_visibility");
- public async Task<GuestAccessEventContent?> GetGuestAccessAsync() =>
- await GetStateAsync<GuestAccessEventContent>("m.room.guest_access");
+ public async Task<RoomGuestAccessEventContent?> GetGuestAccessAsync() =>
+ await GetStateAsync<RoomGuestAccessEventContent>("m.room.guest_access");
- public async Task<RoomCreateEventContent> GetCreateEventAsync() =>
+ public async Task<RoomCreateEventContent?> GetCreateEventAsync() =>
await GetStateAsync<RoomCreateEventContent>("m.room.create");
public async Task<string?> GetRoomType() {
@@ -177,24 +177,23 @@ public class GenericRoom {
await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
new UserIdAndReason { UserId = userId });
- public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) =>
+ public async Task<EventIdResponse?> SendStateEventAsync(string eventType, object content) =>
await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content))
.Content.ReadFromJsonAsync<EventIdResponse>();
- public async Task<EventIdResponse> SendStateEventAsync(string eventType, string stateKey, object content) =>
+ public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) =>
await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content))
.Content.ReadFromJsonAsync<EventIdResponse>();
- public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, EventContent content) {
+ public async Task<EventIdResponse?> SendTimelineEventAsync(string eventType, EventContent content) {
var res = await _httpClient.PutAsJsonAsync(
$"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content, new JsonSerializerOptions {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
- var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
- return resu;
+ return await res.Content.ReadFromJsonAsync<EventIdResponse>();
}
- public async Task<EventIdResponse> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") {
+ public async Task<EventIdResponse?> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") {
var url = await Homeserver.UploadFile(fileName, fileStream);
var content = new RoomMessageEventContent() {
MessageType = messageType,
@@ -205,7 +204,7 @@ public class GenericRoom {
return await SendTimelineEventAsync("m.room.message", content);
}
- public async Task<T> GetRoomAccountDataAsync<T>(string key) {
+ public async Task<T?> GetRoomAccountDataAsync<T>(string key) {
var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}");
if (!res.IsSuccessStatusCode) {
Console.WriteLine($"Failed to get room account data: {await res.Content.ReadAsStringAsync()}");
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 666d2a2..1f3bd37 100644
--- a/LibMatrix/Services/HomeserverProviderService.cs
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -35,20 +35,15 @@ public class HomeserverProviderService {
}
var domain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
- var hc = new MatrixHttpClient { BaseAddress = new Uri(domain) };
AuthenticatedHomeserverGeneric hs;
if (true) {
- hs = new AuthenticatedHomeserverMxApiExtended(homeserver, accessToken);
+ hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverMxApiExtended>(homeserver, accessToken);
}
else {
- hs = new AuthenticatedHomeserverGeneric(homeserver, accessToken);
+ hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverSynapse>(homeserver, accessToken);
}
- hs._httpClient = hc;
- hs._httpClient.Timeout = TimeSpan.FromMinutes(15);
- hs._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
-
// (() => hs.WhoAmI) = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
lock(_authenticatedHomeServerCache)
@@ -59,7 +54,7 @@ public class HomeserverProviderService {
}
public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? proxy = null) {
- var hs = new RemoteHomeServer(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver));
+ var hs = await RemoteHomeServer.Create(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver));
// hs._httpClient.Dispose();
// hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
// hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index 685724b..75545db 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -12,6 +12,9 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new();
public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) {
+ if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
+ if(_wellKnownCache.TryGetValue(homeserver, out var known)) return known;
+ logger?.LogInformation("Resolving homeserver: {}", homeserver);
var res = await _resolveHomeserverFromWellKnown(homeserver);
if (!res.StartsWith("http")) res = "https://" + res;
if (res.EndsWith(":443")) res = res[..^4];
@@ -21,6 +24,7 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) {
if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
var sem = _wellKnownSemaphores.GetOrCreate(homeserver, _ => new SemaphoreSlim(1, 1));
+ if(_wellKnownCache.TryGetValue(homeserver, out var wellKnown)) return wellKnown;
await sem.WaitAsync();
if (_wellKnownCache.TryGetValue(homeserver, out var known)) {
sem.Release();
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index b42bd64..c51fadb 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -39,8 +39,11 @@ public class StateEvent {
public EventContent TypedContent {
get {
+ if(Type == "m.receipt") {
+ return null!;
+ }
try {
- return (EventContent) RawContent.Deserialize(GetType)!;
+ return (EventContent)RawContent.Deserialize(GetType)!;
}
catch (JsonException e) {
Console.WriteLine(e);
@@ -122,6 +125,61 @@ public class StateEvent {
public string cdtype => TypedContent.GetType().Name;
}
+public class StateEventResponse : StateEvent {
+ [JsonPropertyName("origin_server_ts")]
+ public ulong OriginServerTs { get; set; }
+
+ [JsonPropertyName("room_id")]
+ public string RoomId { get; set; }
+
+ [JsonPropertyName("sender")]
+ public string Sender { get; set; }
+
+ [JsonPropertyName("unsigned")]
+ public UnsignedData? Unsigned { get; set; }
+
+ [JsonPropertyName("event_id")]
+ public string EventId { get; set; }
+
+ [JsonPropertyName("user_id")]
+ public string UserId { get; set; }
+
+ [JsonPropertyName("replaces_state")]
+ public new string ReplacesState { get; set; }
+
+ public class UnsignedData {
+ [JsonPropertyName("age")]
+ public ulong? Age { get; set; }
+
+ [JsonPropertyName("redacted_because")]
+ public object? RedactedBecause { get; set; }
+
+ [JsonPropertyName("transaction_id")]
+ public string? TransactionId { get; set; }
+
+ [JsonPropertyName("replaces_state")]
+ public string? ReplacesState { get; set; }
+
+ [JsonPropertyName("prev_sender")]
+ public string? PrevSender { get; set; }
+
+ [JsonPropertyName("prev_content")]
+ public JsonObject? PrevContent { get; set; }
+ }
+}
+
+public class EventList {
+ [JsonPropertyName("events")]
+ public List<StateEventResponse>? Events { get; set; } = new();
+}
+
+public class ChunkedStateEventResponse {
+ [JsonPropertyName("chunk")]
+ public List<StateEventResponse>? Chunk { get; set; } = new();
+}
+
+#region Unused code
+
/*
public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoResolver
{
@@ -150,3 +208,5 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR
}
}
*/
+
+#endregion
|