diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index c729a44..5fd3311 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -5,8 +5,8 @@ using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Web;
using ArcaneLibs.Extensions;
-using LibMatrix.EventTypes.Spec.State;
-using LibMatrix.Extensions;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Filters;
using LibMatrix.Helpers;
using LibMatrix.Homeservers.Extensions.NamedCaches;
@@ -14,7 +14,6 @@ using LibMatrix.Responses;
using LibMatrix.RoomTypes;
using LibMatrix.Services;
using LibMatrix.Utilities;
-using Microsoft.Extensions.Logging.Abstractions;
namespace LibMatrix.Homeservers;
@@ -41,14 +40,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
public string UserId => WhoAmI.UserId;
public string UserLocalpart => UserId.Split(":")[0][1..];
public string ServerName => UserId.Split(":", 2)[1];
+ public string BaseUrl => ClientHttpClient.BaseAddress!.ToString().TrimEnd('/');
[JsonIgnore]
public string AccessToken { get; set; }
- public HsNamedCaches NamedCaches { get; set; } = null!;
+ public HsNamedCaches NamedCaches { get; set; }
public GenericRoom GetRoom(string roomId) {
- if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId));
return new GenericRoom(this, roomId);
}
@@ -171,8 +170,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
try {
return await GetAccountDataAsync<T>(key);
}
- catch (Exception e) {
- return default;
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return default;
+ throw;
}
}
@@ -186,6 +186,16 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
#endregion
+#region MSC 4133
+
+ public async Task UpdateProfilePropertyAsync(string name, object? value) {
+ var caps = await GetCapabilitiesAsync();
+ if (caps is null) throw new Exception("Failed to get capabilities");
+ }
+
+#endregion
+
+ [Obsolete("This method assumes no support for MSC 4069 and MSC 4133")]
public async Task UpdateProfileAsync(UserProfileResponse? newProfile, bool preserveCustomRoomProfile = true) {
if (newProfile is null) return;
Console.WriteLine($"Updating profile for {WhoAmI.UserId} to {newProfile.ToJson(ignoreNull: true)} (preserving room profiles: {preserveCustomRoomProfile})");
@@ -377,11 +387,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
/// <returns>All account data.</returns>
/// <exception cref="Exception"></exception>
public async Task<EventList?> EnumerateAccountData() {
- var syncHelper = new SyncHelper(this);
- syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData);
+ var syncHelper = new SyncHelper(this) {
+ FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData)
+ };
var resp = await syncHelper.SyncAsync();
- if (resp is null) throw new Exception("Sync failed");
- return resp.AccountData;
+ return resp?.AccountData ?? throw new Exception("Sync failed");
}
private Dictionary<string, string>? _namedFilterCache;
@@ -406,4 +416,203 @@ 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 bool? _serverSupportsAuthMedia;
+
+ public async Task<string> GetMediaUrlAsync(MxcUri mxcUri, string? filename = null, int? timeout = null) {
+ if (_serverSupportsAuthMedia == true) return mxcUri.ToDownloadUri(BaseUrl, filename, timeout);
+ if (_serverSupportsAuthMedia == false) return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout);
+
+ try {
+ // Console.WriteLine($"Trying authenticated media URL: {uri}");
+ var res = await ClientHttpClient.SendAsync(new() {
+ Method = HttpMethod.Head,
+ RequestUri = (new Uri(mxcUri.ToDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute))
+ });
+ if (res.IsSuccessStatusCode) {
+ _serverSupportsAuthMedia = true;
+ return mxcUri.ToDownloadUri(BaseUrl, filename, timeout);
+ }
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ //fallback to legacy media
+ try {
+ // Console.WriteLine($"Trying legacy media URL: {uri}");
+ var res = await ClientHttpClient.SendAsync(new() {
+ Method = HttpMethod.Head,
+ RequestUri = new(mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute)
+ });
+ if (res.IsSuccessStatusCode) {
+ _serverSupportsAuthMedia = false;
+ return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout);
+ }
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+ }
+
+ throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+ Error = "Failed to get media URL"
+ };
+ }
+
+ public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
+ var uri = await GetMediaUrlAsync(mxcUri, filename, timeout);
+ var res = await ClientHttpClient.GetAsync(uri);
+ return await res.Content.ReadAsStreamAsync();
+ }
+
+ public async Task<Stream> GetThumbnailStreamAsync(MxcUri mxcUri, int width, int height, string? method = null, int? timeout = null) {
+ var (serverName, mediaId) = 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/v3/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/v3/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
+
+ public Task ReportRoomAsync(string roomId, string reason) =>
+ ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report", new {
+ reason
+ });
+
+ public async Task ReportRoomEventAsync(string roomId, string eventId, string reason, int score = 0, bool ignoreSender = false) {
+ await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report/{eventId}", new {
+ reason,
+ score
+ });
+
+ if (ignoreSender) {
+ var eventContent = await GetRoom(roomId).GetEventAsync(eventId);
+ var sender = eventContent.Sender;
+ await IgnoreUserAsync(sender);
+ }
+ }
+
+ public async Task ReportUserAsync(string userId, string reason, bool ignore = false) {
+ await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/users/{userId}/report", new {
+ reason
+ });
+
+ if (ignore) {
+ await IgnoreUserAsync(userId);
+ }
+ }
+
+ public async Task<IgnoredUserListEventContent> GetIgnoredUserListAsync() {
+ return await GetAccountDataOrNullAsync<IgnoredUserListEventContent>(IgnoredUserListEventContent.EventId) ?? new();
+ }
+
+ public async Task IgnoreUserAsync(string userId, IgnoredUserListEventContent.IgnoredUserContent? content = null) {
+ content ??= new();
+
+ var ignoredUserList = await GetIgnoredUserListAsync();
+ ignoredUserList.IgnoredUsers.TryAdd(userId, content);
+ await SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoredUserList);
+ }
+
+ private class CapabilitiesResponse {
+ [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
|