diff --git a/LibMatrix/Abstractions/VersionedKeyId.cs b/LibMatrix/Abstractions/VersionedKeyId.cs
new file mode 100644
index 0000000..0a6d651
--- /dev/null
+++ b/LibMatrix/Abstractions/VersionedKeyId.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace LibMatrix.Abstractions;
+
+[DebuggerDisplay("{Algorithm}:{KeyId}")]
+[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
+public class VersionedKeyId {
+ public required string Algorithm { get; set; }
+ public required string KeyId { get; set; }
+
+ public static implicit operator VersionedKeyId(string key) {
+ var parts = key.Split(':', 2);
+ if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key));
+ return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] };
+ }
+
+ public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}";
+ public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId);
+ public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId);
+
+ public override bool Equals(object? obj) => obj is VersionedKeyId other && Algorithm == other.Algorithm && KeyId == other.KeyId;
+ public override int GetHashCode() => HashCode.Combine(Algorithm, KeyId);
+}
\ No newline at end of file
diff --git a/LibMatrix/Abstractions/VersionedPublicKey.cs b/LibMatrix/Abstractions/VersionedPublicKey.cs
new file mode 100644
index 0000000..ad6747d
--- /dev/null
+++ b/LibMatrix/Abstractions/VersionedPublicKey.cs
@@ -0,0 +1,16 @@
+namespace LibMatrix.Abstractions;
+
+public class VersionedPublicKey {
+ public required VersionedKeyId KeyId { get; set; }
+ public required string PublicKey { get; set; }
+}
+
+public class VersionedPrivateKey : VersionedPublicKey {
+ public required string PrivateKey { get; set; }
+}
+public class VersionedHomeserverPublicKey : VersionedPublicKey {
+ public required string ServerName { get; set; }
+}
+public class VersionedHomeserverPrivateKey : VersionedPrivateKey {
+ public required string ServerName { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs
index 1b9b4f3..780ac0e 100644
--- a/LibMatrix/Helpers/MessageFormatter.cs
+++ b/LibMatrix/Helpers/MessageFormatter.cs
@@ -30,8 +30,11 @@ public static class MessageFormatter {
public static string HtmlFormatMention(string id, string? displayName = null) => $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
- public static string HtmlFormatMessageLink(string roomId, string eventId, string[]? servers = null, string? displayName = null) {
- if (servers is not { Length: > 0 }) servers = new[] { roomId.Split(':', 2)[1] };
+ public static string HtmlFormatMessageLink(string roomId, string eventId, string[] servers, string? displayName = null) {
+ if (servers is not { Length: > 0 })
+ servers = roomId.Contains(':')
+ ? [roomId.Split(':', 2)[1]]
+ : throw new ArgumentException("Message links must contain a list of via's for v12+ rooms!", nameof(servers));
return $"<a href=\"https://matrix.to/#/{roomId}/{eventId}?via={string.Join("&via=", servers)}\">{displayName ?? eventId}</a>";
}
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 5fd3311..4185353 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -295,7 +295,19 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
public async Task<RoomIdResponse> JoinRoomAsync(string roomId, List<string> homeservers = null, string? reason = null) {
var joinUrl = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(roomId)}";
Console.WriteLine($"Calling {joinUrl} with {homeservers?.Count ?? 0} via's...");
- if (homeservers == null || homeservers.Count == 0) homeservers = new List<string> { roomId.Split(':')[1] };
+ if (homeservers is not { Count: > 0 }) {
+ // Legacy room IDs: !abc:server.xyz
+ if (roomId.Contains(':'))
+ homeservers = [ServerName, roomId.Split(':')[1]];
+ // v12+ room IDs: !<hash>
+ else {
+ homeservers = [ServerName];
+ foreach (var room in await GetJoinedRooms()) {
+ homeservers.Add(await room.GetOriginHomeserverAsync());
+ }
+ }
+ }
+
var fullJoinUrl = $"{joinUrl}?server_name=" + string.Join("&server_name=", homeservers);
var res = await ClientHttpClient.PostAsJsonAsync(fullJoinUrl, new {
reason
@@ -397,14 +409,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
private Dictionary<string, string>? _namedFilterCache;
private Dictionary<string, SyncFilter> _filterCache = new();
- public async Task<JsonObject?> GetCapabilitiesAsync() {
+ public async Task<CapabilitiesResponse> GetCapabilitiesAsync() {
var res = await ClientHttpClient.GetAsync("/_matrix/client/v3/capabilities");
if (!res.IsSuccessStatusCode) {
Console.WriteLine($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}");
throw new InvalidDataException($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}");
}
- return await res.Content.ReadFromJsonAsync<JsonObject>();
+ return await res.Content.ReadFromJsonAsync<CapabilitiesResponse>();
}
public class HsNamedCaches {
@@ -574,9 +586,45 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
await SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoredUserList);
}
- private class CapabilitiesResponse {
+ public class CapabilitiesResponse {
[JsonPropertyName("capabilities")]
- public Dictionary<string, object>? Capabilities { get; set; }
+ public CapabilitiesContents Capabilities { get; set; }
+
+ public class CapabilitiesContents {
+ [JsonPropertyName("m.3pid_changes")]
+ public BooleanCapability? ThreePidChanges { get; set; }
+
+ [JsonPropertyName("m.change_password")]
+ public BooleanCapability? ChangePassword { get; set; }
+
+ [JsonPropertyName("m.get_login_token")]
+ public BooleanCapability? GetLoginToken { get; set; }
+
+ [JsonPropertyName("m.room_versions")]
+ public RoomVersionsCapability? RoomVersions { get; set; }
+
+ [JsonPropertyName("m.set_avatar_url")]
+ public BooleanCapability? SetAvatarUrl { get; set; }
+
+ [JsonPropertyName("m.set_displayname")]
+ public BooleanCapability? SetDisplayName { get; set; }
+
+ [JsonExtensionData]
+ public Dictionary<string, object>? AdditionalCapabilities { get; set; }
+ }
+
+ public class BooleanCapability {
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+ }
+
+ public class RoomVersionsCapability {
+ [JsonPropertyName("default")]
+ public string? Default { get; set; }
+
+ [JsonPropertyName("available")]
+ public Dictionary<string, string>? Available { get; set; }
+ }
}
#region Room Directory/aliases
diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index a2cb12d..9760e20 100644
--- a/LibMatrix/Homeservers/FederationClient.cs
+++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,7 +1,6 @@
-using System.Text.Json.Serialization;
using LibMatrix.Extensions;
+using LibMatrix.Responses.Federation;
using LibMatrix.Services;
-using Microsoft.VisualBasic.CompilerServices;
namespace LibMatrix.Homeservers;
@@ -18,81 +17,7 @@ public class FederationClient {
public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; }
public async Task<ServerVersionResponse> GetServerVersionAsync() => await HttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version");
- public async Task<ServerKeysResponse> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<ServerKeysResponse>("/_matrix/key/v2/server");
+ public async Task<SignedObject<ServerKeysResponse>> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<SignedObject<ServerKeysResponse>>("/_matrix/key/v2/server");
}
-public class ServerKeysResponse {
- [JsonPropertyName("server_name")]
- public string ServerName { get; set; }
- [JsonPropertyName("valid_until_ts")]
- public ulong ValidUntilTs { get; set; }
-
- [JsonIgnore]
- public DateTime ValidUntil {
- get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime;
- set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
- }
-
- [JsonPropertyName("verify_keys")]
- public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new();
-
- [JsonIgnore]
- public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById {
- get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
- set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
- }
-
- [JsonPropertyName("old_verify_keys")]
- public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new();
-
- [JsonIgnore]
- public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById {
- get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
- set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
- }
-
- public class VersionedKeyId {
- public required string Algorithm { get; set; }
- public required string KeyId { get; set; }
-
- public static implicit operator VersionedKeyId(string key) {
- var parts = key.Split(':', 2);
- if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key));
- return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] };
- }
-
- public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}";
- public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId);
- public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId);
- }
-
- public class CurrentVerifyKey {
- [JsonPropertyName("key")]
- public string Key { get; set; }
- }
-
- public class ExpiredVerifyKey : CurrentVerifyKey {
- [JsonPropertyName("expired_ts")]
- public ulong ExpiredTs { get; set; }
-
- [JsonIgnore]
- public DateTime Expired {
- get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime;
- set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
- }
- }
-}
-
-public class ServerVersionResponse {
- [JsonPropertyName("server")]
- public required ServerInfo Server { get; set; }
-
- public class ServerInfo {
- [JsonPropertyName("name")]
- public string Name { get; set; }
-
- [JsonPropertyName("version")]
- public string Version { get; set; }
- }
-}
\ No newline at end of file
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index 62bb48f..7fceb6f 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -12,14 +12,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1"/>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/>
</ItemGroup>
<ItemGroup>
- <!-- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250313-104848" Condition="'$(Configuration)' == 'Release'" />-->
- <!-- <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/>-->
+ <!-- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250313-104848" Condition="'$(Configuration)' == 'Release'" />-->
+ <!-- <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/>-->
<ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/>
</ItemGroup>
diff --git a/LibMatrix/Responses/Federation/ServerKeysResponse.cs b/LibMatrix/Responses/Federation/ServerKeysResponse.cs
new file mode 100644
index 0000000..cb62e34
--- /dev/null
+++ b/LibMatrix/Responses/Federation/ServerKeysResponse.cs
@@ -0,0 +1,55 @@
+using System.Diagnostics;
+using System.Text.Json.Serialization;
+using LibMatrix.Abstractions;
+
+namespace LibMatrix.Responses.Federation;
+
+public class ServerKeysResponse {
+ [JsonPropertyName("server_name")]
+ public string ServerName { get; set; }
+
+ [JsonPropertyName("valid_until_ts")]
+ public ulong ValidUntilTs { get; set; }
+
+ [JsonIgnore]
+ public DateTime ValidUntil {
+ get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime;
+ set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
+ }
+
+ [JsonPropertyName("verify_keys")]
+ public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById {
+ get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
+ set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
+ }
+
+ [JsonPropertyName("old_verify_keys")]
+ public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById {
+ get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
+ set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
+ }
+
+ [DebuggerDisplay("{Key}")]
+ public class CurrentVerifyKey {
+ [JsonPropertyName("key")]
+ public string Key { get; set; }
+ }
+
+ [DebuggerDisplay("{Key} (expired {Expired})")]
+ public class ExpiredVerifyKey : CurrentVerifyKey {
+ [JsonPropertyName("expired_ts")]
+ public ulong ExpiredTs { get; set; }
+
+ [JsonIgnore]
+ public DateTime Expired {
+ get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime;
+ set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
+ }
+ }
+}
diff --git a/LibMatrix/Responses/Federation/ServerVersionResponse.cs b/LibMatrix/Responses/Federation/ServerVersionResponse.cs
new file mode 100644
index 0000000..b09bdd0
--- /dev/null
+++ b/LibMatrix/Responses/Federation/ServerVersionResponse.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses.Federation;
+
+public class ServerVersionResponse {
+ [JsonPropertyName("server")]
+ public required ServerInfo Server { get; set; }
+
+ public class ServerInfo {
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix/Responses/Federation/SignedObject.cs b/LibMatrix/Responses/Federation/SignedObject.cs
new file mode 100644
index 0000000..3f6ffd6
--- /dev/null
+++ b/LibMatrix/Responses/Federation/SignedObject.cs
@@ -0,0 +1,68 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Abstractions;
+using LibMatrix.Homeservers;
+
+namespace LibMatrix.Responses.Federation;
+
+[JsonConverter(typeof(SignedObjectConverterFactory))]
+public class SignedObject<T> {
+ [JsonPropertyName("signatures")]
+ public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<string, Dictionary<VersionedKeyId, string>> SignaturesById {
+ get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value));
+ set => Signatures = value.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (string)key.Key, key => key.Value));
+ }
+
+ [JsonExtensionData]
+ public required JsonObject Content { get; set; }
+
+ [JsonIgnore]
+ public T TypedContent {
+ get => Content.Deserialize<T>() ?? throw new JsonException("Failed to deserialize TypedContent from Content.");
+ set => Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value)) ?? new JsonObject();
+ }
+}
+
+public class SignedObjectConverter<T> : JsonConverter<SignedObject<T>> {
+ public override SignedObject<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+ var jsonObject = JsonSerializer.Deserialize<JsonObject>(ref reader, options);
+ if (jsonObject == null) {
+ throw new JsonException("Failed to deserialize SignedObject, JSON object is null.");
+ }
+
+ var signatures = jsonObject["signatures"] ?? throw new JsonException("Failed to find 'signatures' property in JSON object.");
+ jsonObject.Remove("signatures");
+
+ var signedObject = new SignedObject<T> {
+ Content = jsonObject,
+ Signatures = signatures.Deserialize<Dictionary<string, Dictionary<string, string>>>()
+ ?? throw new JsonException("Failed to deserialize 'signatures' property into Dictionary<string, Dictionary<string, string>>.")
+ };
+
+ return signedObject;
+ }
+
+ public override void Write(Utf8JsonWriter writer, SignedObject<T> value, JsonSerializerOptions options) {
+ var targetObj = value.Content.DeepClone();
+ targetObj["signatures"] = value.Signatures.ToJsonNode();
+ JsonSerializer.Serialize(writer, targetObj, options);
+ }
+}
+
+internal class SignedObjectConverterFactory : JsonConverterFactory {
+ public override bool CanConvert(Type typeToConvert) {
+ if (!typeToConvert.IsGenericType) return false;
+ return typeToConvert.GetGenericTypeDefinition() == typeof(SignedObject<>);
+ }
+
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
+ var wrappedType = typeToConvert.GetGenericArguments()[0];
+ var converter = (JsonConverter)Activator.CreateInstance(typeof(SignedObjectConverter<>).MakeGenericType(wrappedType))!;
+ return converter;
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index fd4db4d..c34dc01 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -70,7 +70,7 @@ public class GenericRoom {
url += "?format=event";
try {
var resp = await Homeserver.ClientHttpClient.GetFromJsonAsync<JsonObject>(url);
- if (resp["type"]?.GetValue<string>() != type)
+ if (resp["type"]?.GetValue<string>() != type || resp["state_key"]?.GetValue<string>() != stateKey)
throw new LibMatrixException() {
Error = "Homeserver returned event type does not match requested type, or server does not support passing `format`.",
ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED
@@ -220,7 +220,16 @@ public class GenericRoom {
var joinUrl = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(RoomId)}";
var materialisedHomeservers = homeservers as string[] ?? homeservers?.ToArray() ?? [];
- if (!materialisedHomeservers.Any()) materialisedHomeservers = [RoomId.Split(':', 2)[1]];
+ if (!materialisedHomeservers.Any())
+ if (RoomId.Contains(':'))
+ materialisedHomeservers = [Homeserver.ServerName, RoomId.Split(':')[1]];
+ // v12+ room IDs: !<hash>
+ else {
+ materialisedHomeservers = [Homeserver.ServerName];
+ foreach (var room in await Homeserver.GetJoinedRooms()) {
+ materialisedHomeservers.Add(await room.GetOriginHomeserverAsync());
+ }
+ }
Console.WriteLine($"Calling {joinUrl} with {materialisedHomeservers.Length} via(s)...");
@@ -275,7 +284,7 @@ public class GenericRoom {
await foreach (var evt in GetMembersEnumerableAsync(membership))
yield return evt.StateKey!;
}
-
+
public async Task<FrozenSet<string>> GetMemberIdsListAsync(string? membership = null) {
var members = await GetMembersListAsync(membership);
return members.Select(x => x.StateKey!).ToFrozenSet();
@@ -462,7 +471,9 @@ public class GenericRoom {
}
public Task<StateEventResponse> GetEventAsync(string eventId, bool includeUnredactedContent = false) =>
- Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}?fi.mau.msc2815.include_unredacted_content={includeUnredactedContent}");
+ Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>(
+ // .ToLower() on boolean here because this query param specifically on synapse is checked as a string rather than a boolean
+ $"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}?fi.mau.msc2815.include_unredacted_content={includeUnredactedContent.ToString().ToLower()}");
public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) {
var data = new { reason };
@@ -606,6 +617,46 @@ public class GenericRoom {
public SpaceRoom AsSpace() => new SpaceRoom(Homeserver, RoomId);
public PolicyRoom AsPolicyRoom() => new PolicyRoom(Homeserver, RoomId);
+
+ private bool IsV12PlusRoomId => !RoomId.Contains(':');
+
+ /// <summary>
+ /// Gets the list of room creators for this room.
+ /// </summary>
+ /// <returns>A list of size 1 for v11 rooms and older, all creators for v12+</returns>
+ public async Task<List<string>> GetRoomCreatorsAsync() {
+ StateEventResponse createEvent;
+ if (IsV12PlusRoomId) {
+ createEvent = await GetEventAsync('$' + RoomId[1..]);
+ }
+ else {
+ createEvent = await GetStateEventAsync("m.room.create");
+ }
+
+ List<string> creators = [createEvent.Sender ?? throw new InvalidDataException("Create event has no sender")];
+
+ if (IsV12PlusRoomId && createEvent.TypedContent is RoomCreateEventContent { AdditionalCreators: { Count: > 0 } additionalCreators }) {
+ creators.AddRange(additionalCreators);
+ }
+
+ return creators;
+ }
+
+ public async Task<string> GetOriginHomeserverAsync() {
+ // pre-v12 room ID
+ if (RoomId.Contains(':')) {
+ var parts = RoomId.Split(':', 2);
+ if (parts.Length == 2) return parts[1];
+ }
+
+ // v12 room ID/fallback
+ var creators = await GetRoomCreatorsAsync();
+ if (creators.Count == 0) {
+ throw new InvalidDataException("Room has no creators, cannot determine origin homeserver");
+ }
+
+ return creators[0].Split(':', 2)[1];
+ }
}
public class RoomIdResponse {
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 36bc828..c984c34 100644
--- a/LibMatrix/Services/HomeserverProviderService.cs
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
using ArcaneLibs.Collections;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
+using LibMatrix.Responses.Federation;
using Microsoft.Extensions.Logging;
namespace LibMatrix.Services;
|