diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 6006048..89e2fdb 100644
--- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -79,6 +79,7 @@ public abstract class PolicyRuleEventContent : EventContent {
/// </summary>
[JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")]
[FriendlyName(Name = "Expires at")]
+ [TableHide]
public DateTime? ExpiryDateTime {
get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime;
set {
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index 49a1b62..eb156b3 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
@@ -57,12 +57,13 @@ public class RoomPowerLevelEventContent : EventContent {
return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0);
}
- public bool UserHasStatePermission(string userId, string eventType) {
+ public bool UserHasStatePermission(string userId, string eventType, bool log = false) {
ArgumentNullException.ThrowIfNull(userId);
var userLevel = GetUserPowerLevel(userId);
var eventLevel = GetStateEventPowerLevel(eventType);
-
- Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
+
+ if (log)
+ Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
return userLevel >= eventLevel;
}
@@ -78,7 +79,7 @@ public class RoomPowerLevelEventContent : EventContent {
if (Events is null) return StateDefault ?? 0;
return Events.TryGetValue(eventType, out var level) ? level : StateDefault ?? 0;
}
-
+
public long GetTimelineEventPowerLevel(string eventType) {
ArgumentNullException.ThrowIfNull(eventType);
if (Events is null) return EventsDefault ?? 0;
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index c9cd260..39eb7e5 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -26,7 +26,8 @@ public class MatrixHttpClient {
EnableMultipleHttp2Connections = true
};
Client = new HttpClient(handler) {
- DefaultRequestVersion = new Version(3, 0)
+ DefaultRequestVersion = new Version(3, 0),
+ Timeout = TimeSpan.FromDays(1)
};
}
catch (PlatformNotSupportedException e) {
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index c729a44..6be49b9 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -406,4 +406,111 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
public NamedFilterCache FilterCache { get; init; }
public NamedFileCache FileCache { get; init; }
}
+
+#region Authenticated Media
+
+ // TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig
+
+ private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) {
+ if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri));
+ var parts = mxcUri[6..].Split('/');
+ if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri));
+ return (parts[0], parts[1]);
+ }
+
+ public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
+ var (serverName, mediaId) = ParseMxcUri(mxcUri);
+ try {
+ var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}";
+ if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+ if (timeout is not null) uri += $"?timeout_ms={timeout}";
+ var res = await ClientHttpClient.GetAsync(uri);
+ return await res.Content.ReadAsStreamAsync();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ //fallback to legacy media
+ try {
+ var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}";
+ if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+ if (timeout is not null) uri += $"?timeout_ms={timeout}";
+ var res = await ClientHttpClient.GetAsync(uri);
+ return await res.Content.ReadAsStreamAsync();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+ Error = "Failed to download media"
+ };
+ // return default;
+ }
+
+ public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) {
+ var (serverName, mediaId) = ParseMxcUri(mxcUri);
+ try {
+ var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}");
+ uri = uri.AddQuery("width", width.ToString());
+ uri = uri.AddQuery("height", height.ToString());
+ if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+ if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+ var res = await ClientHttpClient.GetAsync(uri.ToString());
+ return await res.Content.ReadAsStreamAsync();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ //fallback to legacy media
+ try {
+ var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}");
+ uri = uri.AddQuery("width", width.ToString());
+ uri = uri.AddQuery("height", height.ToString());
+ if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+ if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+ var res = await ClientHttpClient.GetAsync(uri.ToString());
+ return await res.Content.ReadAsStreamAsync();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+ Error = "Failed to download media"
+ };
+ // return default;
+ }
+
+ public async Task<Dictionary<string, JsonValue>?> GetUrlPreviewAsync(string url) {
+ try {
+ var res = await ClientHttpClient.GetAsync($"/_matrix/client/v1/media/preview_url?url={HttpUtility.UrlEncode(url)}");
+ return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+ }
+
+ //fallback to legacy media
+ try {
+ var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}");
+ return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+ }
+
+ throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+ Error = "Failed to download URL preview"
+ };
+ }
+
+#endregion
}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index ecf3e3a..f9e3d04 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -107,6 +107,7 @@ public class RemoteHomeserver {
#endregion
+ [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)]
public string? ResolveMediaUri(string? mxcUri) {
if (mxcUri is null) return null;
if (mxcUri.StartsWith("https://")) return mxcUri;
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index b906f08..a1ef617 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -210,10 +210,11 @@ public class GenericRoom {
public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) {
if (checkIfAlreadyMember)
try {
- _ = await GetCreateEventAsync();
- return new RoomIdResponse {
- RoomId = RoomId
- };
+ var ser = await GetStateEventOrNullAsync(RoomMemberEventContent.EventId, Homeserver.UserId);
+ if (ser?.TypedContent is RoomMemberEventContent { Membership: "join" })
+ return new RoomIdResponse {
+ RoomId = RoomId
+ };
}
catch { } //ignore
@@ -316,6 +317,7 @@ public class GenericRoom {
public Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() =>
GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels");
+ [Obsolete("This method will be merged into GetNameAsync() in the future.")]
public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) {
try {
return await GetNameAsync();
@@ -352,22 +354,6 @@ public class GenericRoom {
return Task.WhenAll(tasks);
}
- public async Task<string?> GetResolvedRoomAvatarUrlAsync(bool useOriginHomeserver = false) {
- var avatar = await GetAvatarUrlAsync();
- if (avatar?.Url is null) return null;
- if (!avatar.Url.StartsWith("mxc://")) return avatar.Url;
- if (useOriginHomeserver)
- try {
- var hs = avatar.Url.Split('/', 3)[1];
- return await new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance).ResolveMediaUri(hs, avatar.Url);
- }
- catch (Exception e) {
- Console.WriteLine(e);
- }
-
- return Homeserver.ResolveMediaUri(avatar.Url);
- }
-
#endregion
#region Simple calls
diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
index 401223c..2819f80 100644
--- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
+++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
@@ -78,19 +78,7 @@ public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config
var username = _config.TestUsername;
var password = _config.TestPassword;
-
- LoginResponse reg;
- try {
- reg = await rhs.LoginAsync(username, password);
- }
- catch (MatrixException e) {
- if (e.ErrorCode == "M_FORBIDDEN") {
- await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!");
- reg = await rhs.RegisterAsync(username, password, "Unit tests!");
- }
- else throw new Exception("Failed to log in", e);
- }
-
- return (username, password, reg.AccessToken);
+ var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+ return ("", "", "");
}
}
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
new file mode 100644
index 0000000..712e45a
--- /dev/null
+++ b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
@@ -0,0 +1,56 @@
+using ArcaneLibs.Extensions;
+using ArcaneLibs.Extensions.Streams;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using LibMatrix.Tests.Abstractions;
+using LibMatrix.Tests.Fixtures;
+using Xunit.Abstractions;
+using Xunit.Microsoft.DependencyInjection.Abstracts;
+
+namespace LibMatrix.Tests.Tests;
+
+public class AuthMediaTests : TestBed<TestFixture> {
+ private readonly TestFixture _fixture;
+ private readonly HomeserverResolverService _resolver;
+ private readonly Config _config;
+ private readonly HomeserverProviderService _provider;
+ private readonly HomeserverAbstraction _hsAbstraction;
+
+ public AuthMediaTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
+ _fixture = fixture;
+ _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}");
+ _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}");
+ _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
+ _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
+ }
+
+ [Fact]
+ public async Task UploadFileAsync() {
+ var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+ var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+ Assert.NotNull(mxcUri);
+ }
+
+ [Fact]
+ public async Task DownloadFileAsync() {
+ var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+ var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+ Assert.NotNull(mxcUri);
+
+ var file = await hs.GetMediaStreamAsync(mxcUri);
+ Assert.NotNull(file);
+
+ var data = file!.ReadToEnd().AsString();
+ Assert.Equal("LibMatrix test file", data);
+ }
+
+ [SkippableFact(typeof(LibMatrixException))] // This test will fail if the homeserver does not support URL previews
+ public async Task GetUrlPreviewAsync() {
+ var hs = await _hsAbstraction.GetConfiguredHomeserver();
+ var preview = await hs.GetUrlPreviewAsync("https://matrix.org");
+
+ Assert.NotNull(preview);
+ }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
index 633c842..69e6231 100644
--- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
@@ -20,17 +20,36 @@ public class AuthTests : TestBed<TestFixture> {
[Fact]
public async Task LoginWithPassword() {
- var credentials = await _hsAbstraction.GetKnownCredentials();
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
+
+ // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!);
+ var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+ var username = Guid.NewGuid().ToString();
+ var password = Guid.NewGuid().ToString();
- var login = await _provider.Login(_config.TestHomeserver!, credentials.username, credentials.password);
+ var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+
+ var login = await _provider.Login(_config.TestHomeserver!, username, password);
Assert.NotNull(login);
Assert.NotNull(login.AccessToken);
}
[Fact]
public async Task LoginWithToken() {
- var credentials = await _hsAbstraction.GetKnownCredentials();
- var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, credentials.token);
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
+ Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
+
+ // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!);
+ var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+ var username = Guid.NewGuid().ToString();
+ var password = Guid.NewGuid().ToString();
+
+ var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+
+ var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, reg.AccessToken);
Assert.NotNull(hs);
Assert.NotNull(hs.WhoAmI);
hs.WhoAmI.VerifyRequiredFields();
diff --git a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
index 03f3c24..20f975e 100644
--- a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
@@ -18,13 +18,13 @@ public class RemoteHomeserverTests : TestBed<TestFixture> {
_provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
}
- [Fact]
- public async Task ResolveMedia() {
- var hs = await _provider.GetRemoteHomeserver("matrix.org");
- var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
-
- Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
- }
+ // [Fact]
+ // public async Task ResolveMedia() {
+ // var hs = await _provider.GetRemoteHomeserver("matrix.org");
+ // var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
+ //
+ // Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
+ // }
[Fact]
public async Task ResolveRoomAliasAsync() {
diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
deleted file mode 100644
index 1c5747c..0000000
--- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// using System.Diagnostics;
-// using LibMatrix.Helpers;
-// using LibMatrix.Services;
-// using LibMatrix.Tests.Abstractions;
-// using LibMatrix.Tests.Fixtures;
-// using Microsoft.Extensions.Logging;
-// using Xunit.Abstractions;
-// using Xunit.Microsoft.DependencyInjection.Abstracts;
-//
-// namespace LibMatrix.Tests.Tests;
-//
-// public class TestCleanup : TestBed<TestFixture> {
-// private readonly HomeserverAbstraction _hsAbstraction;
-// private readonly ILogger<TestCleanup> _logger;
-//
-// public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
-// // _fixture = fixture;
-// _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}");
-// _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
-// }
-//
-// [SkippableFact(typeof(MatrixException))]
-// public async Task Cleanup() {
-// // Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
-// // Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
-// // Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
-//
-// var hs = await _hsAbstraction.GetConfiguredHomeserver();
-// Assert.NotNull(hs);
-//
-// var syncHelper = new SyncHelper(hs, _logger) {
-// Timeout = 3000
-// };
-// _testOutputHelper.WriteLine("Starting sync loop");
-// var cancellationTokenSource = new CancellationTokenSource();
-// var sw = Stopwatch.StartNew();
-// syncHelper.SyncReceivedHandlers.Add(async response => {
-// // if (sw.ElapsedMilliseconds >= 3000) {
-// // _testOutputHelper.WriteLine("Cancelling sync loop");
-//
-// var tasks = (await hs.GetJoinedRooms()).Select(async room => {
-// _logger.LogInformation("Leaving room: {}", room.RoomId);
-// await room.LeaveAsync();
-// await room.ForgetAsync();
-// return room;
-// }).ToList();
-// await Task.WhenAll(tasks);
-//
-// // cancellationTokenSource.Cancel();
-// // }
-//
-// sw.Restart();
-// if (response.Rooms?.Leave is { Count: > 0 }) {
-// // foreach (var room in response.Rooms.Leave) {
-// // await hs.GetRoom(room.Key).ForgetAsync();
-// // }
-// var tasks2 = response.Rooms.Leave.Select(async room => {
-// await hs.GetRoom(room.Key).ForgetAsync();
-// return room;
-// }).ToList();
-// await Task.WhenAll(tasks2);
-// }
-// });
-// await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token);
-//
-// Assert.NotNull(hs);
-// await hs.Logout();
-// }
-// }
\ No newline at end of file
|