diff --git a/.idea/.idea.LibMatrix/.idea/vcs.xml b/.idea/.idea.LibMatrix/.idea/vcs.xml
index 35eb1dd..63ea78b 100644
--- a/.idea/.idea.LibMatrix/.idea/vcs.xml
+++ b/.idea/.idea.LibMatrix/.idea/vcs.xml
@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
+ <mapping directory="$PROJECT_DIR$/ArcaneLibs" vcs="Git" />
</component>
</project>
\ No newline at end of file
diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject a2c73b7b89ca8910a0f9a9856bb06a6d5f49ce0
+Subproject 7cbf235cceb82dc711622314c49edfc7e2614dc
diff --git a/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs b/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs
index a31cbbb..a1ebd79 100644
--- a/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs
+++ b/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
namespace LibMatrix.EventTypes.Common;
[MatrixEvent(EventName = EventId)]
-public class MjolnirShortcodeEventContent : TimelineEventContent {
+public class MjolnirShortcodeEventContent : EventContent {
public const string EventId = "org.matrix.mjolnir.shortcode";
[JsonPropertyName("shortcode")]
diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
index 9602bf3..d1cf8be 100644
--- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -34,9 +34,29 @@ public class RoomMessageEventContent : TimelineEventContent {
[JsonPropertyName("info")]
public FileInfoStruct? FileInfo { get; set; }
-
+
[JsonIgnore]
- public string BodyWithoutReplyFallback => Body.Split('\n').SkipWhile(x => x.StartsWith(">")).SkipWhile(x=>x.Trim().Length == 0).Aggregate((x, y) => $"{x}\n{y}");
+ public string BodyWithoutReplyFallback {
+ get {
+ var parts = Body
+ .Split('\n')
+ .SkipWhile(x => x.StartsWith(">"))
+ .SkipWhile(x => x.Trim().Length == 0)
+ .ToList();
+ return parts.Count > 0 ? parts.Aggregate((x, y) => $"{x}\n{y}") : Body;
+ }
+ }
+
+ [JsonPropertyName("m.mentions")]
+ public MentionsStruct? Mentions { get; set; }
+
+ public class MentionsStruct {
+ [JsonPropertyName("user_ids")]
+ public List<string>? Users { get; set; }
+
+ [JsonPropertyName("room")]
+ public bool? Room { get; set; }
+ }
public class FileInfoStruct {
[JsonPropertyName("mimetype")]
@@ -47,10 +67,10 @@ public class RoomMessageEventContent : TimelineEventContent {
[JsonPropertyName("thumbnail_url")]
public string? ThumbnailUrl { get; set; }
-
+
[JsonPropertyName("w")]
public int? Width { get; set; }
-
+
[JsonPropertyName("h")]
public int? Height { get; set; }
}
diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 6f8c194..d75b19f 100644
--- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -35,7 +35,6 @@ public class RoomPolicyRuleEventContent : PolicyRuleEventContent {
[DebuggerDisplay("""{GetType().Name.Replace("PolicyRuleEventContent", ""),nq} policy matching {Entity}, Reason: {Reason}""")]
public abstract class PolicyRuleEventContent : EventContent {
// public PolicyRuleEventContent() => Console.WriteLine($"init policy {GetType().Name}");
- private string? _reason;
/// <summary>
/// Entity this ban applies to, can use * and ? as globs.
@@ -52,17 +51,7 @@ public abstract class PolicyRuleEventContent : EventContent {
/// </summary>
[JsonPropertyName("reason")]
[FriendlyName(Name = "Reason")]
- public virtual string? Reason {
- get =>
- // Console.WriteLine($"Read policy reason: {_reason}");
- _reason;
- set =>
- // Console.WriteLine($"Set policy reason: {value}");
- // if(init)
- // Console.WriteLine(string.Join('\n', Environment.StackTrace.Split('\n')[..5]));
- // init = true;
- _reason = value;
- }
+ public string? Reason { get; set; }
/// <summary>
/// Suggested action to take
@@ -93,29 +82,46 @@ public abstract class PolicyRuleEventContent : EventContent {
}
}
+ [JsonPropertyName("org.matrix.msc4205.hashes")]
+ [TableHide]
+ public PolicyHash? Hashes { get; set; }
+
public string GetDraupnir2StateKey() => Convert.ToBase64String(SHA256.HashData($"{Entity}{Recommendation}".AsBytes().ToArray()));
public Regex? GetEntityRegex() => Entity is null ? null : new(Entity.Replace(".", "\\.").Replace("*", ".*").Replace("?", "."));
public bool IsGlobRule() =>
- Entity != null
+ !string.IsNullOrWhiteSpace(Entity)
&& (Entity.Contains('*') || Entity.Contains('?'));
- public bool EntityMatches(string entity) =>
- Entity != null
- && (
- Entity == entity
- || (
- IsGlobRule()
- ? GetEntityRegex()!.IsMatch(entity)
- : entity == Entity
- )
- );
+ public bool EntityMatches(string entity) {
+ if (string.IsNullOrWhiteSpace(entity)) return false;
+
+ if (!string.IsNullOrWhiteSpace(Entity)) {
+ // Check if entity is equal regardless of glob check
+ var match = Entity == entity
+ || (IsGlobRule() && GetEntityRegex()!.IsMatch(entity));
+ if (match) return match;
+ }
+
+ if (Hashes is not null) {
+ if (!string.IsNullOrWhiteSpace(Hashes.Sha256)) {
+ var hash = SHA256.HashData(entity.AsBytes().ToArray());
+ var match = Convert.ToBase64String(hash) == Hashes.Sha256;
+ if (match) return match;
+ }
+ }
+
+ return false;
+ }
public string? GetNormalizedRecommendation() {
if (Recommendation is "m.ban" or "org.matrix.mjolnir.ban")
return PolicyRecommendationTypes.Ban;
+ if (Recommendation is "m.takedown" or "org.matrix.msc4204.takedown")
+ return "m.takedown";
+
return Recommendation;
}
}
@@ -130,6 +136,13 @@ public static class PolicyRecommendationTypes {
/// Mute this user
/// </summary>
public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending
+
+ public static string Takedown = "m.takedown"; //unstable prefix: org.matrix.msc4204.takedown
+}
+
+public class PolicyHash {
+ [JsonPropertyName("sha256")]
+ public string? Sha256 { get; set; }
}
// public class PolicySchemaDefinition {
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
index 16cfcb0..8edf4a7 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
@@ -8,4 +8,10 @@ public class RoomHistoryVisibilityEventContent : EventContent {
[JsonPropertyName("history_visibility")]
public required string HistoryVisibility { get; set; }
+
+ public static class HistoryVisibilityTypes {
+ public const string WorldReadable = "world_readable";
+ public const string Invited = "invited";
+ public const string Shared = "shared";
+ }
}
\ No newline at end of file
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
new file mode 100644
index 0000000..80e254f
--- /dev/null
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.EventTypes.Spec.State.RoomInfo;
+
+[MatrixEvent(EventName = EventId)]
+public class RoomPolicyServerEventContent : EventContent {
+ public const string EventId = "org.matrix.msc4284.policy";
+
+ [JsonPropertyName("via")]
+ public string? Via { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs
new file mode 100644
index 0000000..055f22d
--- /dev/null
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.EventTypes.Spec.State.RoomInfo;
+
+[MatrixEvent(EventName = EventId)]
+public class RoomRedactionEventContent : EventContent {
+ public const string EventId = "m.room.redaction";
+
+ [JsonPropertyName("reason")]
+ public string? Reason { get; set; }
+
+ /// <summary>
+ /// Required in room version 11
+ /// </summary>
+ [JsonPropertyName("redacts")]
+ public string? Redacts { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs
new file mode 100644
index 0000000..6f8d44b
--- /dev/null
+++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -0,0 +1,20 @@
+using LibMatrix.Homeservers;
+
+namespace LibMatrix.Federation;
+
+public class AuthenticatedFederationClient : FederationClient {
+
+ public class AuthenticatedFederationConfiguration {
+
+ }
+ public AuthenticatedFederationClient(string federationEndpoint, AuthenticatedFederationConfiguration config, string? proxy = null) : base(federationEndpoint, proxy)
+ {
+
+ }
+
+ // public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) {
+ // var response = await GetAsync<UserDeviceListResponse>($"/_matrix/federation/v1/user/devices/{userId}", accessToken);
+ // return response;
+ // }
+
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/Extensions/Ed25519Extensions.cs b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
new file mode 100644
index 0000000..69baf58
--- /dev/null
+++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
@@ -0,0 +1,8 @@
+using LibMatrix.FederationTest.Utilities;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace LibMatrix.Federation.Extensions;
+
+public static class Ed25519Extensions {
+ public static string ToUnpaddedBase64(this Ed25519PublicKeyParameters key) => UnpaddedBase64.Encode(key.GetEncoded());
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj
new file mode 100644
index 0000000..78086bb
--- /dev/null
+++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
+ <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0-preview.5.25277.114" />
+ </ItemGroup>
+
+</Project>
diff --git a/LibMatrix.Federation/Utilities/JsonSigning.cs b/LibMatrix.Federation/Utilities/JsonSigning.cs
new file mode 100644
index 0000000..c727cde
--- /dev/null
+++ b/LibMatrix.Federation/Utilities/JsonSigning.cs
@@ -0,0 +1,108 @@
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Extensions;
+using LibMatrix.FederationTest.Utilities;
+using LibMatrix.Homeservers;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Math.EC.Rfc8032;
+
+namespace LibMatrix.Federation.Utilities;
+
+public static class JsonSigning { }
+
+[JsonConverter(typeof(SignedObjectConverterFactory))]
+public class SignedObject<T> {
+ [JsonPropertyName("signatures")]
+ public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<string, Dictionary<ServerKeysResponse.VersionedKeyId, string>> VerifyKeysById {
+ get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (ServerKeysResponse.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();
+ }
+}
+
+// Content needs to be merged at toplevel
+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;
+ }
+}
+
+public static class ObjectExtensions {
+ public static SignedObject<T> Sign<T>(this SignedObject<T> content, string serverName, string keyName, Ed25519PrivateKeyParameters key) {
+ var signResult = Sign(content.Content, serverName, keyName, key);
+ var signedObject = new SignedObject<T> {
+ Signatures = content.Signatures,
+ Content = signResult.Content
+ };
+
+ if (!signedObject.Signatures.ContainsKey(serverName))
+ signedObject.Signatures[serverName] = new Dictionary<string, string>();
+
+ signedObject.Signatures[serverName][keyName] = signResult.Signatures[serverName][keyName];
+ return signedObject;
+ }
+
+ public static SignedObject<T> Sign<T>(this T content, string serverName, string keyName, Ed25519PrivateKeyParameters key) {
+ SignedObject<T> signedObject = new() {
+ Signatures = [],
+ Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(content)) ?? new JsonObject(),
+ };
+
+ var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(signedObject.Content);
+ var signature = new byte[Ed25519.SignatureSize];
+ key.Sign(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signature, 0);
+
+ if (!signedObject.Signatures.ContainsKey(serverName))
+ signedObject.Signatures[serverName] = new Dictionary<string, string>();
+
+ signedObject.Signatures[serverName][keyName] = UnpaddedBase64.Encode(signature);
+ return signedObject;
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/Utilities/UnpaddedBase64.cs b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs
new file mode 100644
index 0000000..06f84b2
--- /dev/null
+++ b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs
@@ -0,0 +1,17 @@
+namespace LibMatrix.FederationTest.Utilities;
+
+public static class UnpaddedBase64 {
+ public static string Encode(byte[] data) {
+ return Convert.ToBase64String(data).TrimEnd('=');
+ }
+
+ public static byte[] Decode(string base64) {
+ string paddedBase64 = base64;
+ switch (paddedBase64.Length % 4) {
+ case 2: paddedBase64 += "=="; break;
+ case 3: paddedBase64 += "="; break;
+ }
+
+ return Convert.FromBase64String(paddedBase64);
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
new file mode 100644
index 0000000..fc402b7
--- /dev/null
+++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -0,0 +1,71 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using Microsoft.Extensions.Primitives;
+
+namespace LibMatrix.Federation;
+
+public class XMatrixAuthorizationScheme {
+ public class XMatrixAuthorizationHeader {
+ public const string Scheme = "X-Matrix";
+
+ [JsonPropertyName("origin")]
+ public required string Origin { get; set; }
+
+ [JsonPropertyName("destination")]
+ public required string Destination { get; set; }
+
+ [JsonPropertyName("key")]
+ public required string Key { get; set; }
+
+ [JsonPropertyName("sig")]
+ public required string Signature { get; set; }
+
+ public static XMatrixAuthorizationHeader FromHeaderValue(AuthenticationHeaderValue header) {
+ if (header.Scheme != Scheme)
+ throw new LibMatrixException() {
+ Error = $"Expected authentication scheme of {Scheme}, got {header.Scheme}",
+ ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED
+ };
+
+ if (string.IsNullOrWhiteSpace(header.Parameter))
+ throw new LibMatrixException() {
+ Error = $"Expected authentication header to have a value.",
+ ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED
+ };
+
+ var headerValues = new StringValues(header.Parameter);
+ foreach (var value in headerValues) {
+ Console.WriteLine(headerValues.ToJson());
+ }
+
+ return new() {
+ Destination = "",
+ Key = "",
+ Origin = "",
+ Signature = ""
+ };
+ }
+
+ public string ToHeaderValue() => $"{Scheme} origin=\"{Origin}\", destination=\"{Destination}\", key=\"{Key}\", sig=\"{Signature}\"";
+ }
+
+ public class XMatrixRequestSignature {
+ [JsonPropertyName("method")]
+ public required string Method { get; set; }
+
+ [JsonPropertyName("uri")]
+ public required string Uri { get; set; }
+
+ [JsonPropertyName("origin")]
+ public required string OriginServerName { get; set; }
+
+ [JsonPropertyName("destination")]
+ public required string DestinationServerName { get; set; }
+
+ [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public JsonObject? Content { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix.sln b/LibMatrix.sln
index 5feeeee..d6c1c0f 100644
--- a/LibMatrix.sln
+++ b/LibMatrix.sln
@@ -1,148 +1,310 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{32C0F9AC-AF7D-4476-A269-99ACA000EF9F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{93D00F03-02FF-4C2A-8917-6863D6E633D9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{51F770AF-C0C6-4247-A358-82DF323F473D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{8A8E8B67-9351-471E-A502-AF7FAE596CB4}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{567DABFB-611E-4779-9F39-1D4A5B8F0247}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{28AFDFEF-7597-4450-B999-87C22C24DCD8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{F80D5395-28E3-4C34-9662-A890A215DDA2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{90A38896-993A-49F1-903B-8C989D8C9B3A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{6AE504F8-734B-456B-8BEA-DE37639CB3A7}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{AAAA5609-D8C1-401E-BB5C-724EFE3955FB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{7E15D295-B938-409C-98F6-A22ABC7F3005}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{FBC6F613-4E0B-4A90-8854-31887A796EEF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{D632374A-70A1-4954-8F2D-C0B511A24426}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{91E41EE3-687B-4CFD-94C4-028D09BACFAB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{4D411F2D-A97D-4485-A318-ED98B96B6CF6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Utilities\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{7F36D200-FE91-4761-B681-BEE091FB953F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{69CCB780-50CC-4E75-B794-932E44ACA80F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator", "Utilities\LibMatrix.TestDataGenerator\LibMatrix.TestDataGenerator.csproj", "{C087F8AC-B438-4980-9A79-1E16D0CFB67A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.Build.0 = Release|Any CPU
- {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.Build.0 = Release|Any CPU
- {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.Build.0 = Release|Any CPU
- {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.Build.0 = Release|Any CPU
- {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.Build.0 = Release|Any CPU
- {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.Build.0 = Release|Any CPU
- {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.Build.0 = Release|Any CPU
- {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.Build.0 = Release|Any CPU
- {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.Build.0 = Release|Any CPU
- {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.Build.0 = Release|Any CPU
- {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.Build.0 = Release|Any CPU
- {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.Build.0 = Release|Any CPU
- {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.Build.0 = Release|Any CPU
- {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.Build.0 = Release|Any CPU
- {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.Build.0 = Release|Any CPU
- {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.Build.0 = Release|Any CPU
- {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {93D00F03-02FF-4C2A-8917-6863D6E633D9} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {51F770AF-C0C6-4247-A358-82DF323F473D} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {8A8E8B67-9351-471E-A502-AF7FAE596CB4} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {567DABFB-611E-4779-9F39-1D4A5B8F0247} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {28AFDFEF-7597-4450-B999-87C22C24DCD8} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {F80D5395-28E3-4C34-9662-A890A215DDA2} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
- {7E15D295-B938-409C-98F6-A22ABC7F3005} = {AAAA5609-D8C1-401E-BB5C-724EFE3955FB}
- {D632374A-70A1-4954-8F2D-C0B511A24426} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {91E41EE3-687B-4CFD-94C4-028D09BACFAB} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {4D411F2D-A97D-4485-A318-ED98B96B6CF6} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {7F36D200-FE91-4761-B681-BEE091FB953F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {69CCB780-50CC-4E75-B794-932E44ACA80F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {C087F8AC-B438-4980-9A79-1E16D0CFB67A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{32C0F9AC-AF7D-4476-A269-99ACA000EF9F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{93D00F03-02FF-4C2A-8917-6863D6E633D9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{51F770AF-C0C6-4247-A358-82DF323F473D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{8A8E8B67-9351-471E-A502-AF7FAE596CB4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{567DABFB-611E-4779-9F39-1D4A5B8F0247}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{28AFDFEF-7597-4450-B999-87C22C24DCD8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{F80D5395-28E3-4C34-9662-A890A215DDA2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{90A38896-993A-49F1-903B-8C989D8C9B3A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{6AE504F8-734B-456B-8BEA-DE37639CB3A7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{AAAA5609-D8C1-401E-BB5C-724EFE3955FB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{7E15D295-B938-409C-98F6-A22ABC7F3005}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{FBC6F613-4E0B-4A90-8854-31887A796EEF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{D632374A-70A1-4954-8F2D-C0B511A24426}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{91E41EE3-687B-4CFD-94C4-028D09BACFAB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{4D411F2D-A97D-4485-A318-ED98B96B6CF6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Utilities\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{7F36D200-FE91-4761-B681-BEE091FB953F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{69CCB780-50CC-4E75-B794-932E44ACA80F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator", "Utilities\LibMatrix.TestDataGenerator\LibMatrix.TestDataGenerator.csproj", "{C087F8AC-B438-4980-9A79-1E16D0CFB67A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Federation", "LibMatrix.Federation\LibMatrix.Federation.csproj", "{0E6CF267-14FD-43D7-81CB-EE020FD1D106}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x64.Build.0 = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x86.Build.0 = Debug|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x64.ActiveCfg = Release|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x64.Build.0 = Release|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x86.ActiveCfg = Release|Any CPU
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x86.Build.0 = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x64.Build.0 = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x86.Build.0 = Debug|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x64.ActiveCfg = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x64.Build.0 = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x86.ActiveCfg = Release|Any CPU
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x86.Build.0 = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x64.Build.0 = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x86.Build.0 = Debug|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x64.ActiveCfg = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x64.Build.0 = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x86.ActiveCfg = Release|Any CPU
+ {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x86.Build.0 = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x64.Build.0 = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x86.Build.0 = Debug|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x64.ActiveCfg = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x64.Build.0 = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x86.ActiveCfg = Release|Any CPU
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x86.Build.0 = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x64.Build.0 = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x86.Build.0 = Debug|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x64.ActiveCfg = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x64.Build.0 = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x86.ActiveCfg = Release|Any CPU
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x86.Build.0 = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x64.Build.0 = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x86.Build.0 = Debug|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.Build.0 = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x64.ActiveCfg = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x64.Build.0 = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x86.ActiveCfg = Release|Any CPU
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x86.Build.0 = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x64.Build.0 = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x86.Build.0 = Debug|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x64.ActiveCfg = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x64.Build.0 = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x86.ActiveCfg = Release|Any CPU
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x86.Build.0 = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x64.Build.0 = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x86.Build.0 = Debug|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x64.ActiveCfg = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x64.Build.0 = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x86.ActiveCfg = Release|Any CPU
+ {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x86.Build.0 = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x64.Build.0 = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x86.Build.0 = Debug|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x64.ActiveCfg = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x64.Build.0 = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x86.ActiveCfg = Release|Any CPU
+ {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x86.Build.0 = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x64.Build.0 = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x86.Build.0 = Debug|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x64.ActiveCfg = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x64.Build.0 = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x86.ActiveCfg = Release|Any CPU
+ {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x86.Build.0 = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x64.Build.0 = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x86.Build.0 = Debug|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x64.ActiveCfg = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x64.Build.0 = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x86.ActiveCfg = Release|Any CPU
+ {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x86.Build.0 = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x64.Build.0 = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x86.Build.0 = Debug|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x64.ActiveCfg = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x64.Build.0 = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x86.ActiveCfg = Release|Any CPU
+ {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x86.Build.0 = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x64.Build.0 = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x86.Build.0 = Debug|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x64.ActiveCfg = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x64.Build.0 = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x86.ActiveCfg = Release|Any CPU
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x86.Build.0 = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x64.Build.0 = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x86.Build.0 = Debug|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x64.ActiveCfg = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x64.Build.0 = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x86.ActiveCfg = Release|Any CPU
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x86.Build.0 = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x64.Build.0 = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x86.Build.0 = Debug|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x64.ActiveCfg = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x64.Build.0 = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x86.ActiveCfg = Release|Any CPU
+ {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x86.Build.0 = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x64.Build.0 = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x86.Build.0 = Debug|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x64.ActiveCfg = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x64.Build.0 = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x86.ActiveCfg = Release|Any CPU
+ {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x86.Build.0 = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x64.Build.0 = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x86.Build.0 = Debug|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x64.ActiveCfg = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x64.Build.0 = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x86.ActiveCfg = Release|Any CPU
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x86.Build.0 = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x64.Build.0 = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x86.Build.0 = Debug|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x64.ActiveCfg = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x64.Build.0 = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x86.ActiveCfg = Release|Any CPU
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x86.Build.0 = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x64.Build.0 = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x86.Build.0 = Debug|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x64.ActiveCfg = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x64.Build.0 = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x86.ActiveCfg = Release|Any CPU
+ {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {93D00F03-02FF-4C2A-8917-6863D6E633D9} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {51F770AF-C0C6-4247-A358-82DF323F473D} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {8A8E8B67-9351-471E-A502-AF7FAE596CB4} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {567DABFB-611E-4779-9F39-1D4A5B8F0247} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {28AFDFEF-7597-4450-B999-87C22C24DCD8} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {F80D5395-28E3-4C34-9662-A890A215DDA2} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F}
+ {7E15D295-B938-409C-98F6-A22ABC7F3005} = {AAAA5609-D8C1-401E-BB5C-724EFE3955FB}
+ {D632374A-70A1-4954-8F2D-C0B511A24426} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {91E41EE3-687B-4CFD-94C4-028D09BACFAB} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {4D411F2D-A97D-4485-A318-ED98B96B6CF6} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {7F36D200-FE91-4761-B681-BEE091FB953F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {69CCB780-50CC-4E75-B794-932E44ACA80F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {C087F8AC-B438-4980-9A79-1E16D0CFB67A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF}
+ EndGlobalSection
+EndGlobal
diff --git a/LibMatrix/Extensions/CanonicalJsonSerializer.cs b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
index 55a4b1a..ae535aa 100644
--- a/LibMatrix/Extensions/CanonicalJsonSerializer.cs
+++ b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Reflection;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
using ArcaneLibs.Extensions;
@@ -57,6 +58,14 @@ public static class CanonicalJsonSerializer {
// public static String Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => JsonSerializer.Serialize(value, jsonTypeInfo, _options);
// public static String Serialize(Object value, JsonTypeInfo jsonTypeInfo)
+ public static byte[] SerializeToUtf8Bytes<T>(T value, JsonSerializerOptions? options = null) {
+ var newOptions = MergeOptions(null);
+ return JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here...
+ .SortProperties()!
+ .CanonicalizeNumbers()!
+ .ToJsonString(newOptions).AsBytes().ToArray();
+ }
+
#endregion
// ReSharper disable once UnusedType.Local
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index baa4a2c..671566f 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -101,22 +101,22 @@ public class MatrixHttpClient {
responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}
catch (Exception e) {
- if (attempt >= 5) {
- Console.WriteLine(
- $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
- throw;
- }
-
- if (e is TaskCanceledException or TimeoutException or HttpRequestException) {
- if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) {
- await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken);
- request.ResetSendStatus();
- return await SendUnhandledAsync(request, cancellationToken, attempt + 1);
- }
- }
- else if (!e.ToString().StartsWith("TypeError: NetworkError"))
- Console.WriteLine(
- $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
+ // if (attempt >= 5) {
+ // Console.WriteLine(
+ // $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
+ // throw;
+ // }
+ //
+ // if (e is TaskCanceledException or TimeoutException or HttpRequestException) {
+ // if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) {
+ // await Task.Delay(Random.Shared.Next(100, 1000), cancellationToken);
+ // request.ResetSendStatus();
+ // return await SendUnhandledAsync(request, cancellationToken, attempt + 1);
+ // }
+ // }
+ // else if (!e.ToString().StartsWith("TypeError: NetworkError"))
+ // Console.WriteLine(
+ // $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
throw;
}
@@ -286,9 +286,9 @@ public class MatrixHttpClient {
return await SendAsync(request, cancellationToken);
}
- public async Task DeleteAsync(string url) {
+ public async Task<HttpResponseMessage> DeleteAsync(string url) {
var request = new HttpRequestMessage(HttpMethod.Delete, url);
- await SendAsync(request);
+ return await SendAsync(request);
}
public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) {
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index 5e2b1b7..6f55739 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -37,6 +37,10 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
return this;
}
+ public static string GetColoredBody(string color, string body) {
+ return $"<font color=\"{color}\">{body}</font>";
+ }
+
public MessageBuilder WithColoredBody(string color, string body) {
Content.Body += body;
Content.FormattedBody += $"<font color=\"{color}\">{body}</font>";
@@ -94,6 +98,16 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
public MessageBuilder WithMention(string id, string? displayName = null) {
Content.Body += $"@{displayName ?? id}";
Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
+ if (id == "@room") {
+ Content.Mentions ??= new();
+ Content.Mentions.Room = true;
+ }
+ else if (id.StartsWith('@')) {
+ Content.Mentions ??= new();
+ Content.Mentions.Users ??= new();
+ Content.Mentions.Users.Add(id);
+ }
+
return this;
}
diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
new file mode 100644
index 0000000..bef7568
--- /dev/null
+++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -0,0 +1,173 @@
+using System.Runtime.Intrinsics.X86;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+
+namespace LibMatrix.Helpers;
+
+public class RoomBuilder {
+ public string? Type { get; set; }
+ public string Version { get; set; } = "11";
+ public RoomNameEventContent Name { get; set; } = new();
+ public RoomTopicEventContent Topic { get; set; } = new();
+ public RoomAvatarEventContent Avatar { get; set; } = new();
+ public RoomCanonicalAliasEventContent CanonicalAlias { get; set; } = new();
+ public string AliasLocalPart { get; set; } = string.Empty;
+ public bool IsFederatable { get; set; } = true;
+ public long OwnPowerLevel { get; set; } = MatrixConstants.MaxSafeJsonInteger;
+
+ public RoomJoinRulesEventContent JoinRules { get; set; } = new() {
+ JoinRule = RoomJoinRulesEventContent.JoinRules.Public
+ };
+
+ public RoomHistoryVisibilityEventContent HistoryVisibility { get; set; } = new() {
+ HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared
+ };
+
+ /// <summary>
+ /// State events to be sent *before* room access is configured. Keep this small!
+ /// </summary>
+ public List<StateEvent> ImportantState { get; set; } = [];
+
+ /// <summary>
+ /// State events to be sent *after* room access is configured, but before invites are sent.
+ /// </summary>
+ public List<StateEvent> InitialState { get; set; } = [];
+
+ /// <summary>
+ /// Users to invite, with optional reason
+ /// </summary>
+ public Dictionary<string, string?> Invites { get; set; } = new();
+
+ public RoomPowerLevelEventContent PowerLevels { get; init; } = new() {
+ EventsDefault = 0,
+ UsersDefault = 0,
+ Kick = 50,
+ Invite = 50,
+ Ban = 50,
+ Redact = 50,
+ StateDefault = 50,
+ NotificationsPl = new() {
+ Room = 50
+ },
+ Users = [],
+ Events = new Dictionary<string, long> {
+ { RoomAvatarEventContent.EventId, 50 },
+ { RoomCanonicalAliasEventContent.EventId, 50 },
+ { RoomEncryptionEventContent.EventId, 100 },
+ { RoomHistoryVisibilityEventContent.EventId, 100 },
+ { RoomNameEventContent.EventId, 50 },
+ { RoomPowerLevelEventContent.EventId, 100 },
+ { RoomServerAclEventContent.EventId, 100 },
+ { RoomTombstoneEventContent.EventId, 100 },
+ { RoomPolicyServerEventContent.EventId, 100 }
+ }
+ };
+
+ public async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) {
+ var crq = new CreateRoomRequest() {
+ PowerLevelContentOverride = new() {
+ EventsDefault = 1000000,
+ UsersDefault = 1000000,
+ Kick = 1000000,
+ Invite = 1000000,
+ Ban = 1000000,
+ Redact = 1000000,
+ StateDefault = 1000000,
+ NotificationsPl = new() {
+ Room = 1000000
+ },
+ Users = new Dictionary<string, long>() {
+ { homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger }
+ },
+ Events = new Dictionary<string, long> {
+ { RoomAvatarEventContent.EventId, 1000000 },
+ { RoomCanonicalAliasEventContent.EventId, 1000000 },
+ { RoomEncryptionEventContent.EventId, 1000000 },
+ { RoomHistoryVisibilityEventContent.EventId, 1000000 },
+ { RoomNameEventContent.EventId, 1000000 },
+ { RoomPowerLevelEventContent.EventId, 1000000 },
+ { RoomServerAclEventContent.EventId, 1000000 },
+ { RoomTombstoneEventContent.EventId, 1000000 },
+ { RoomPolicyServerEventContent.EventId, 1000000 }
+ }
+ },
+ Visibility = "private",
+ RoomVersion = Version
+ };
+
+ if (!string.IsNullOrWhiteSpace(Type))
+ crq.CreationContent.Add("type", Type);
+
+ if (!IsFederatable)
+ crq.CreationContent.Add("m.federate", false);
+
+ var room = await homeserver.CreateRoom(crq);
+
+ await SetBasicRoomInfoAsync(room);
+ await SetStatesAsync(room, ImportantState);
+ await SetAccessAsync(room);
+ await SetStatesAsync(room, InitialState);
+ await SendInvites(room);
+
+ return room;
+ }
+
+ private async Task SendInvites(GenericRoom room) {
+ if (Invites.Count == 0) return;
+
+ var inviteTasks = Invites.Select(async kvp => {
+ try {
+ await room.InviteUserAsync(kvp.Key, kvp.Value);
+ }
+ catch (MatrixException e) {
+ Console.Error.WriteLine("Failed to invite {0} to {1}: {2}", kvp.Key, room.RoomId, e.Message);
+ }
+ });
+
+ await Task.WhenAll(inviteTasks);
+ }
+
+ private async Task SetStatesAsync(GenericRoom room, List<StateEvent> state) {
+ foreach (var ev in state) {
+ await (string.IsNullOrWhiteSpace(ev.StateKey)
+ ? room.SendStateEventAsync(ev.Type, ev.RawContent)
+ : room.SendStateEventAsync(ev.Type, ev.StateKey, ev.RawContent));
+ }
+ }
+
+ private async Task SetBasicRoomInfoAsync(GenericRoom room) {
+ if (!string.IsNullOrWhiteSpace(Name.Name))
+ await room.SendStateEventAsync(RoomNameEventContent.EventId, Name);
+
+ if (!string.IsNullOrWhiteSpace(Topic.Topic))
+ await room.SendStateEventAsync(RoomTopicEventContent.EventId, Topic);
+
+ if (!string.IsNullOrWhiteSpace(Avatar.Url))
+ await room.SendStateEventAsync(RoomAvatarEventContent.EventId, Avatar);
+
+ if (!string.IsNullOrWhiteSpace(AliasLocalPart))
+ CanonicalAlias.Alias = $"#{AliasLocalPart}:{room.Homeserver.ServerName}";
+
+ if (!string.IsNullOrWhiteSpace(CanonicalAlias.Alias)) {
+ await room.Homeserver.SetRoomAliasAsync(CanonicalAlias.Alias!, room.RoomId);
+ await room.SendStateEventAsync(RoomCanonicalAliasEventContent.EventId, CanonicalAlias);
+ }
+ }
+
+ private async Task SetAccessAsync(GenericRoom room) {
+ PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel;
+ await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, PowerLevels);
+
+ if (!string.IsNullOrWhiteSpace(HistoryVisibility.HistoryVisibility))
+ await room.SendStateEventAsync(RoomHistoryVisibilityEventContent.EventId, HistoryVisibility);
+
+ if (!string.IsNullOrWhiteSpace(JoinRules.JoinRuleValue))
+ await room.SendStateEventAsync(RoomJoinRulesEventContent.EventId, JoinRules);
+ }
+}
+
+public class MatrixConstants {
+ public const long MaxSafeJsonInteger = 9007199254740991L; // 2^53 - 1
+}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 6f2cacc..c8e2928 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging;
namespace LibMatrix.Helpers;
public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null, IStorageProvider? storageProvider = null) {
- private readonly Func<SyncResponse?, Task<SyncResponse?>> _msc4222EmulationSyncProcessor = new Msc4222EmulationSyncProcessor(homeserver).EmulateMsc4222;
+ private readonly Func<SyncResponse?, Task<SyncResponse?>> _msc4222EmulationSyncProcessor = new Msc4222EmulationSyncProcessor(homeserver, logger).EmulateMsc4222;
private SyncFilter? _filter;
private string? _namedFilterName;
@@ -25,7 +25,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
public string? Since { get; set; }
public int Timeout { get; set; } = 30000;
public string? SetPresence { get; set; }
-
+
/// <summary>
/// Disabling this uses a technically slower code path, useful for checking whether delay comes from waiting for server or deserialising responses
/// </summary>
@@ -37,11 +37,11 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
field = value;
if (value) {
AsyncSyncPreprocessors.Add(_msc4222EmulationSyncProcessor);
- Console.WriteLine($"Added MSC4222 emulation sync processor");
+ logger?.LogInformation($"Added MSC4222 emulation sync processor");
}
else {
AsyncSyncPreprocessors.Remove(_msc4222EmulationSyncProcessor);
- Console.WriteLine($"Removed MSC4222 emulation sync processor");
+ logger?.LogInformation($"Removed MSC4222 emulation sync processor");
}
}
} = false;
@@ -121,7 +121,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
}
if (storageProvider is null) {
- var res = await SyncAsyncInternal(cancellationToken, noDelay);
+ var res = await SyncAsyncInternal(cancellationToken, noDelay);
if (res is null) return null;
if (UseMsc4222StateAfter) res.Msc4222Method = SyncResponse.Msc4222SyncType.Server;
@@ -186,13 +186,12 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
else {
var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None);
if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request");
- logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed);
+ var receivedTime = sw.Elapsed;
var deserializeSw = Stopwatch.StartNew();
- // var jsonResp = await httpResp.Content.ReadFromJsonAsync<JsonObject>(cancellationToken: cancellationToken ?? CancellationToken.None);
- // var resp = jsonResp.Deserialize<SyncResponse>();
resp = await httpResp.Content.ReadFromJsonAsync(cancellationToken: cancellationToken ?? CancellationToken.None,
jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse);
- logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed);
+ logger?.LogInformation("Deserialized sync response: {} bytes, {} response time, {} deserialize time, {} total", httpResp.GetContentLength(), receivedTime,
+ deserializeSw.Elapsed, sw.Elapsed);
}
var timeToWait = MinimumDelay.Subtract(sw.Elapsed);
diff --git a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
index 6cb42ca..e34b5cf 100644
--- a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
+++ b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
@@ -1,11 +1,13 @@
using System.Diagnostics;
+using System.Timers;
using ArcaneLibs.Extensions;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
+using Microsoft.Extensions.Logging;
namespace LibMatrix.Helpers.SyncProcessors;
-public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver) {
+public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver, ILogger? logger) {
private static bool StateEventsMatch(StateEventResponse a, StateEventResponse b) {
return a.Type == b.Type && a.StateKey == b.StateKey;
}
@@ -22,12 +24,13 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
resp.Rooms.Join?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true
|| resp.Rooms.Leave?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true
) {
- Console.WriteLine($"Msc4222EmulationSyncProcessor.EmulateMsc4222 determined that no emulation is needed in {sw.Elapsed}");
+ logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug,
+ "Msc4222EmulationSyncProcessor.EmulateMsc4222 determined that no emulation is needed in {elapsed}", sw.Elapsed);
return resp;
}
resp = await EmulateMsc4222Internal(resp, sw);
-
+
return SimpleSyncProcessors.FillRoomIds(resp);
}
@@ -49,7 +52,10 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
}
}
- Console.WriteLine($"Msc4222EmulationSyncProcessor.EmulateMsc4222 processed {resp.Rooms?.Join?.Count}/{resp.Rooms?.Leave?.Count} rooms in {sw.Elapsed} (modified: {modified})");
+ logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug,
+ "Msc4222EmulationSyncProcessor.EmulateMsc4222 processed {joinCount}/{leaveCount} rooms in {elapsed} (modified: {modified})",
+ resp.Rooms?.Join?.Count ?? 0, resp.Rooms?.Leave?.Count ?? 0, sw.Elapsed, modified);
+
if (modified)
resp.Msc4222Method = SyncResponse.Msc4222SyncType.Emulated;
@@ -90,7 +96,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
}
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e);
}
}
@@ -103,12 +109,12 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
// .Join(oldState, x => (x.Type, x.StateKey), y => (y.Type, y.StateKey), (x, y) => x)
.IntersectBy(oldState.Select(s => (s.Type, s.StateKey)), s => (s.Type, s.StateKey))
.ToList();
-
+
data.State = null;
return true;
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e);
}
var tasks = oldState
@@ -117,7 +123,8 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!);
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get state event {oldEvt.Type}/{oldEvt.StateKey} for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}",
+ oldEvt.Type, oldEvt.StateKey, roomId, e);
return oldEvt;
}
});
@@ -150,7 +157,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
return true;
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e);
}
var oldState = new List<StateEventResponse>();
@@ -173,7 +180,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
}
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e);
}
}
@@ -185,7 +192,8 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!);
}
catch (Exception e) {
- Console.WriteLine($"Msc4222Emulation: Failed to get state event {oldEvt.Type}/{oldEvt.StateKey} for room {roomId}, state may be incomplete!\n{e}");
+ logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}",
+ oldEvt.Type, oldEvt.StateKey, roomId, e);
return oldEvt;
}
});
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 55899de..5fd3311 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -578,4 +578,41 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
[JsonPropertyName("capabilities")]
public Dictionary<string, object>? Capabilities { get; set; }
}
+
+#region Room Directory/aliases
+
+ public async Task SetRoomAliasAsync(string roomAlias, string roomId) {
+ var resp = await ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/directory/room/{HttpUtility.UrlEncode(roomAlias)}", new RoomIdResponse() {
+ RoomId = roomId
+ });
+ if (!resp.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}");
+ throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}");
+ }
+ }
+
+ public async Task DeleteRoomAliasAsync(string roomAlias) {
+ var resp = await ClientHttpClient.DeleteAsync("/_matrix/client/v3/directory/room/" + HttpUtility.UrlEncode(roomAlias));
+ if (!resp.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}");
+ throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}");
+ }
+ }
+
+ public async Task<RoomAliasesResponse> GetLocalRoomAliasesAsync(string roomId) {
+ var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{HttpUtility.UrlEncode(roomId)}/aliases");
+ if (!resp.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}");
+ throw new InvalidDataException($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}");
+ }
+
+ return await resp.Content.ReadFromJsonAsync<RoomAliasesResponse>() ?? throw new Exception("Failed to get room aliases?");
+ }
+
+ public class RoomAliasesResponse {
+ [JsonPropertyName("aliases")]
+ public required List<string> Aliases { get; set; }
+ }
+
+#endregion
}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index 617b737..a2cb12d 100644
--- a/LibMatrix/Homeservers/FederationClient.cs
+++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using LibMatrix.Extensions;
using LibMatrix.Services;
+using Microsoft.VisualBasic.CompilerServices;
namespace LibMatrix.Homeservers;
@@ -17,6 +18,70 @@ 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 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 {
diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
index 62b291b..5a4acf7 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
@@ -1,27 +1,5 @@
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters;
public class SynapseAdminLocalUserQueryFilter {
- public string UserIdContains { get; set; } = "";
- public string NameContains { get; set; } = "";
- public string CanonicalAliasContains { get; set; } = "";
- public string VersionContains { get; set; } = "";
- public string CreatorContains { get; set; } = "";
- public string EncryptionContains { get; set; } = "";
- public string JoinRulesContains { get; set; } = "";
- public string GuestAccessContains { get; set; } = "";
- public string HistoryVisibilityContains { get; set; } = "";
- public bool Federatable { get; set; } = true;
- public bool Public { get; set; } = true;
-
- public int JoinedMembersGreaterThan { get; set; }
- public int JoinedMembersLessThan { get; set; } = int.MaxValue;
-
- public int JoinedLocalMembersGreaterThan { get; set; }
- public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue;
- public int StateEventsGreaterThan { get; set; }
- public int StateEventsLessThan { get; set; } = int.MaxValue;
-
- public bool CheckFederation { get; set; }
- public bool CheckPublic { get; set; }
}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
index 777c04a..cee3d8d 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -183,6 +183,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
#region Users
public async IAsyncEnumerable<SynapseAdminUserListResult.SynapseAdminUserListResultUser> SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250,
+ string orderBy = "name", string dir = "f",
SynapseAdminLocalUserQueryFilter? localFilter = null) {
// TODO: implement filters
string? from = null;
@@ -190,6 +191,9 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
var url = new Uri("/_synapse/admin/v3/users", UriKind.Relative);
url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString());
if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from);
+ if (!string.IsNullOrWhiteSpace(orderBy)) url = url.AddQuery("order_by", orderBy);
+ if (!string.IsNullOrWhiteSpace(dir)) url = url.AddQuery("dir", dir);
+
Console.WriteLine($"--- ADMIN Querying User List with URL: {url} ---");
// TODO: implement URI methods in http client
var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminUserListResult>(url.ToString());
diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
index d9a6acd..6933622 100644
--- a/LibMatrix/Responses/CreateRoomRequest.cs
+++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -47,6 +47,8 @@ public class CreateRoomRequest {
[JsonPropertyName("invite")]
public List<string>? Invite { get; set; }
+ public string? RoomVersion { get; set; }
+
/// <summary>
/// For use only when you can't use the CreationContent property
/// </summary>
diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/Responses/EventIdResponse.cs
index 6a04229..9e23210 100644
--- a/LibMatrix/EventIdResponse.cs
+++ b/LibMatrix/Responses/EventIdResponse.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace LibMatrix;
+namespace LibMatrix.Responses;
public class EventIdResponse {
[JsonPropertyName("event_id")]
diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
index 2f78932..1944276 100644
--- a/LibMatrix/Responses/LoginResponse.cs
+++ b/LibMatrix/Responses/LoginResponse.cs
@@ -19,11 +19,6 @@ public class LoginResponse {
[JsonPropertyName("user_id")]
public string UserId { get; set; }
-
- // public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) {
- // var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver);
- // await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(Homeserver, AccessToken, proxy);
- // }
}
public class LoginRequest {
diff --git a/LibMatrix/MessagesResponse.cs b/LibMatrix/Responses/MessagesResponse.cs
index 526da74..4912add 100644
--- a/LibMatrix/MessagesResponse.cs
+++ b/LibMatrix/Responses/MessagesResponse.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace LibMatrix;
+namespace LibMatrix.Responses;
public class MessagesResponse {
[JsonPropertyName("start")]
diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/Responses/UserIdAndReason.cs
index 99c9eaf..176cf7c 100644
--- a/LibMatrix/UserIdAndReason.cs
+++ b/LibMatrix/Responses/UserIdAndReason.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace LibMatrix;
+namespace LibMatrix.Responses;
internal class UserIdAndReason(string userId = null!, string reason = null!) {
[JsonPropertyName("user_id")]
diff --git a/LibMatrix/WhoAmIResponse.cs b/LibMatrix/Responses/WhoAmIResponse.cs
index 10fff35..db47152 100644
--- a/LibMatrix/WhoAmIResponse.cs
+++ b/LibMatrix/Responses/WhoAmIResponse.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace LibMatrix;
+namespace LibMatrix.Responses;
public class WhoAmIResponse {
[JsonPropertyName("user_id")]
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index bc1bc90..fd4db4d 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -1,6 +1,5 @@
using System.Collections.Frozen;
using System.Net.Http.Json;
-using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -12,6 +11,7 @@ using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Filters;
using LibMatrix.Helpers;
using LibMatrix.Homeservers;
+using LibMatrix.Responses;
namespace LibMatrix.RoomTypes;
@@ -461,8 +461,8 @@ public class GenericRoom {
}
}
- public Task<StateEventResponse> GetEventAsync(string eventId) =>
- Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}");
+ 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}");
public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) {
var data = new { reason };
diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
index 0c74be5..96abd77 100644
--- a/LibMatrix/RoomTypes/SpaceRoom.cs
+++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -1,5 +1,6 @@
using ArcaneLibs.Extensions;
using LibMatrix.Homeservers;
+using LibMatrix.Responses;
namespace LibMatrix.RoomTypes;
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index 53cd2dd..94a3826 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -44,7 +44,17 @@ public class HomeserverResolverService {
return res;
});
}
-
+
+ private async Task<T?> GetFromJsonAsync<T>(string url) {
+ try {
+ return await _httpClient.GetFromJsonAsync<T>(url);
+ }
+ catch (Exception e) {
+ _logger.LogWarning(e, "Failed to get JSON from {url}", url);
+ return default;
+ }
+ }
+
private async Task<string?> _tryResolveClientEndpoint(string homeserver) {
ArgumentNullException.ThrowIfNull(homeserver);
_logger.LogTrace("Resolving client well-known: {homeserver}", homeserver);
@@ -52,14 +62,20 @@ public class HomeserverResolverService {
homeserver = homeserver.TrimEnd('/');
// check if homeserver has a client well-known
if (homeserver.StartsWith("https://")) {
- clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
+ clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
+
+ if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions"))
+ return homeserver;
}
else if (homeserver.StartsWith("http://")) {
- clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
+ clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
+
+ if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions"))
+ return homeserver;
}
else {
- clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client");
- clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client");
+ clientWellKnown ??= await GetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client");
+ clientWellKnown ??= await GetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client");
if (clientWellKnown is null) {
if (await MatrixHttpClient.CheckSuccessStatus($"https://{homeserver}/_matrix/client/versions"))
@@ -84,14 +100,14 @@ public class HomeserverResolverService {
homeserver = homeserver.TrimEnd('/');
// check if homeserver has a server well-known
if (homeserver.StartsWith("https://")) {
- serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
+ serverWellKnown = await GetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
}
else if (homeserver.StartsWith("http://")) {
- serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
+ serverWellKnown = await GetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server");
}
else {
- serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server");
- serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server");
+ serverWellKnown ??= await GetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server");
+ serverWellKnown ??= await GetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server");
}
_logger.LogInformation("Server well-known for {hs}: {json}", homeserver, serverWellKnown?.ToJson() ?? "null");
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index e2ac87e..af25805 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -121,7 +121,16 @@ public class StateEvent {
public string InternalContentTypeName => TypedContent?.GetType().Name ?? "null";
public static bool TypeKeyPairMatches(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey;
- public static bool Equals(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey && x.RawContent.Equals(y.RawContent);
+ public static bool Equals(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey && x.EventId == y.EventId;
+
+ /// <summary>
+ /// Compares two state events for deep equality, including type, state key, and raw content.
+ /// If you trust the server, use Equals instead, as that compares by event ID instead of raw content.
+ /// </summary>
+ /// <param name="x"></param>
+ /// <param name="y"></param>
+ /// <returns></returns>
+ public static bool DeepEquals(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey && JsonNode.DeepEquals(x.RawContent, y.RawContent);
}
public class StateEventResponse : StateEvent {
diff --git a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
index e1da3d5..2a25056 100644
--- a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
+++ b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
@@ -1,3 +1,5 @@
+using LibMatrix.Responses;
+
namespace LibMatrix.Tests.DataTests;
public static class WhoAmITests {
diff --git a/Utilities/LibMatrix.FederationTest/.gitignore b/Utilities/LibMatrix.FederationTest/.gitignore
new file mode 100644
index 0000000..4a96773
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/.gitignore
@@ -0,0 +1 @@
+.keys/
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs b/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs
new file mode 100644
index 0000000..33d0b99
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs
@@ -0,0 +1,41 @@
+using LibMatrix.Federation.Extensions;
+using LibMatrix.Federation.Utilities;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+[Route("_matrix/key/v2/")]
+public class FederationKeysController(FederationTestConfiguration config, FederationKeyStore keyStore) {
+ static FederationKeysController() {
+ Console.WriteLine("INFO | FederationKeysController initialized.");
+ }
+
+ private static SignedObject<ServerKeysResponse>? _cachedServerKeysResponse;
+ private static SemaphoreSlim _serverKeyCacheLock = new SemaphoreSlim(1, 1);
+
+ [HttpGet("server")]
+ public async Task<SignedObject<ServerKeysResponse>> GetServerKeys() {
+ await _serverKeyCacheLock.WaitAsync();
+ if (_cachedServerKeysResponse == null || _cachedServerKeysResponse.TypedContent.ValidUntil < DateTime.Now + TimeSpan.FromSeconds(30)) {
+ var keys = keyStore.GetCurrentSigningKey();
+ _cachedServerKeysResponse = new ServerKeysResponse() {
+ ValidUntil = DateTime.Now + TimeSpan.FromMinutes(1),
+ ServerName = config.ServerName,
+ OldVerifyKeys = [],
+ VerifyKeysById = new() {
+ {
+ new() { Algorithm = "ed25519", KeyId = "0" }, new ServerKeysResponse.CurrentVerifyKey() {
+ Key = keys.publicKey.ToUnpaddedBase64(),
+ }
+ }
+ }
+ }.Sign(config.ServerName, new ServerKeysResponse.VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey);
+ }
+ _serverKeyCacheLock.Release();
+
+ return _cachedServerKeysResponse;
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs b/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs
new file mode 100644
index 0000000..2c3aaa3
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs
@@ -0,0 +1,18 @@
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+[Route("_matrix/federation/v1/")]
+public class FederationVersionController : ControllerBase {
+ [HttpGet("version")]
+ public ServerVersionResponse GetVersion() {
+ return new ServerVersionResponse {
+ Server = new() {
+ Name = "LibMatrix.Federation",
+ Version = "0.0.0",
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
new file mode 100644
index 0000000..4a6bc87
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
+using LibMatrix.Federation.Utilities;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+public class TestController(FederationTestConfiguration config, FederationKeyStore keyStore) : ControllerBase {
+ static TestController() {
+ Console.WriteLine("INFO | TestController initialized.");
+ }
+
+ [HttpGet("/test")]
+ public async Task<JsonObject> GetTest() {
+ var hc = new MatrixHttpClient() {
+ BaseAddress = new Uri("https://matrix.rory.gay")
+ };
+
+ var keyId = new ServerKeysResponse.VersionedKeyId() {
+ Algorithm = "ed25519",
+ KeyId = "0"
+ };
+
+ var signatureData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() {
+ Method = "GET",
+ Uri = "/_matrix/federation/v1/user/devices/@emma:rory.gay",
+ OriginServerName = config.ServerName,
+ DestinationServerName = "rory.gay"
+ }
+ .Sign(config.ServerName, keyId, keyStore.GetCurrentSigningKey().privateKey);
+
+ var signature = signatureData.Signatures[config.ServerName][keyId];
+ var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() {
+ Origin = config.ServerName,
+ Destination = "rory.gay",
+ Key = keyId,
+ Signature = signature
+ }.ToHeaderValue();
+
+ var req = new HttpRequestMessage(HttpMethod.Get, "/_matrix/federation/v1/user/devices/@emma:rory.gay");
+ req.Headers.Add("Authorization", headerValue);
+
+ var response = await hc.SendAsync(req);
+ var content = await response.Content.ReadFromJsonAsync<JsonObject>();
+ return content!;
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs b/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs
new file mode 100644
index 0000000..28fca8d
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs
@@ -0,0 +1,19 @@
+using LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+[Route(".well-known/")]
+public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase {
+ static WellKnownController() {
+ Console.WriteLine("INFO | WellKnownController initialized.");
+ }
+ [HttpGet("matrix/server")]
+ public ServerWellKnown GetMatrixServerWellKnown() {
+ // {Request.Headers["X-Forwarded-Proto"].FirstOrDefault(Request.Scheme)}://
+ return new() {
+ Homeserver = $"{Request.Headers["X-Forwarded-Host"].FirstOrDefault(Request.Host.Host)}:{Request.Headers["X-Forwarded-Port"].FirstOrDefault("443")}",
+ };
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
new file mode 100644
index 0000000..c54ba8d
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\LibMatrix.Federation\LibMatrix.Federation.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
new file mode 100644
index 0000000..adc809f
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -0,0 +1,34 @@
+using LibMatrix.FederationTest.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+builder.Services.AddHttpLogging(options => {
+ options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
+ options.RequestHeaders.Add("X-Forwarded-Proto");
+ options.RequestHeaders.Add("X-Forwarded-Host");
+ options.RequestHeaders.Add("X-Forwarded-Port");
+});
+
+builder.Services.AddSingleton<FederationTestConfiguration>();
+builder.Services.AddSingleton<FederationKeyStore>();
+
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (true || app.Environment.IsDevelopment()) {
+ app.MapOpenApi();
+}
+
+app.UseAuthorization();
+
+app.MapControllers();
+// app.UseHttpLogging();
+
+
+app.Run();
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
new file mode 100644
index 0000000..f24d14e
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -0,0 +1,31 @@
+using LibMatrix.FederationTest.Utilities;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationKeyStore(FederationTestConfiguration config) {
+ static FederationKeyStore() {
+ Console.WriteLine("INFO | FederationKeyStore initialized.");
+ }
+
+ private static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) currentKeyPair = default;
+ public (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GetCurrentSigningKey() {
+ if (currentKeyPair != default) {
+ return currentKeyPair;
+ }
+
+ if(!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath);
+
+ var privateKeyPath = Path.Combine(config.KeyStorePath, "signing.key");
+ if (!File.Exists(privateKeyPath)) {
+ var keyPair = Ed25519Utils.GenerateKeyPair();
+ File.WriteAllBytes(privateKeyPath, keyPair.privateKey.GetEncoded());
+ return keyPair;
+ }
+
+ var privateKeyBytes = File.ReadAllBytes(privateKeyPath);
+ var privateKey = Ed25519Utils.LoadPrivateKeyFromEncoded(privateKeyBytes);
+ var publicKey = privateKey.GeneratePublicKey();
+ return currentKeyPair = (privateKey, publicKey);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
new file mode 100644
index 0000000..353ddf5
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
@@ -0,0 +1,10 @@
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationTestConfiguration {
+ public FederationTestConfiguration(IConfiguration configurationSection) {
+ configurationSection.GetRequiredSection("FederationTest").Bind(this);
+ }
+
+ public string ServerName { get; set; } = "localhost";
+ public string KeyStorePath { get; set; } = "./.keys";
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
new file mode 100644
index 0000000..bb57d51
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
@@ -0,0 +1,28 @@
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+
+namespace LibMatrix.FederationTest.Utilities;
+
+public class Ed25519Utils {
+ public static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GenerateKeyPair() {
+ Console.WriteLine("Generating new Ed25519 key pair!");
+ var keyPairGen = new Ed25519KeyPairGenerator();
+ keyPairGen.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGen.GenerateKeyPair();
+
+ var privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
+ var publicKey = (Ed25519PublicKeyParameters)keyPair.Public;
+
+ return (privateKey, publicKey);
+ }
+
+ public static Ed25519PublicKeyParameters LoadPublicKeyFromEncoded(string key) {
+ var keyBytes = Convert.FromBase64String(key);
+ return new Ed25519PublicKeyParameters(keyBytes, 0);
+ }
+
+ public static Ed25519PrivateKeyParameters LoadPrivateKeyFromEncoded(byte[] key) {
+ return new Ed25519PrivateKeyParameters(key, 0);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/appsettings.Development.json b/Utilities/LibMatrix.FederationTest/appsettings.Development.json
new file mode 100644
index 0000000..b6c6151
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/appsettings.Development.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.AspNetCore.Routing": "Warning"
+ }
+ },
+ "FederationTest": {
+ "ServerName": "libmatrix-fed-test.rory.gay",
+ "KeyStorePath": "./.keys"
+ }
+}
diff --git a/Utilities/LibMatrix.FederationTest/appsettings.json b/Utilities/LibMatrix.FederationTest/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
index 74c70a3..485c028 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
@@ -2,6 +2,7 @@ using System.Collections.Frozen;
using System.Text.Json.Nodes;
using LibMatrix.HomeserverEmulator.Extensions;
using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
using Microsoft.AspNetCore.Mvc;
namespace LibMatrix.HomeserverEmulator.Controllers.Rooms;
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
index 40f3667..339e686 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
using Microsoft.AspNetCore.Mvc;
namespace LibMatrix.HomeserverEmulator.Controllers;
diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs
index c6abde2..71ecbed 100644
--- a/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs
@@ -1,5 +1,6 @@
using LibMatrix.EventTypes.Spec;
using LibMatrix.Homeservers;
+using LibMatrix.Responses;
using LibMatrix.RoomTypes;
namespace LibMatrix.Utilities.Bot.Interfaces;
diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs
index 4c6b462..d0a93a4 100644
--- a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs
@@ -25,7 +25,7 @@ public class CommandListenerHostedService(
private FrozenSet<ICommand> _commands = null!;
private Task? _listenerTask;
- private CancellationTokenSource _cts = new();
+ private readonly CancellationTokenSource _cts = new();
private long _startupTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
/// <summary>Triggered when the application host is ready to start the service.</summary>
@@ -61,7 +61,6 @@ public class CommandListenerHostedService(
Timeout = config.SyncConfiguration.Timeout ?? 30_000,
MinimumDelay = config.SyncConfiguration.MinimumSyncTime ?? TimeSpan.Zero,
SetPresence = config.SyncConfiguration.Presence ?? botConfig.Presence,
-
};
syncHelper.SyncReceivedHandlers.Add(async sync => {
|