From 9f8d0c85c54b4715974994aea52562072d6f1751 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Wed, 31 Jan 2024 12:09:28 +0100 Subject: Better sync filter support, named filters, error handling --- .../Homeservers/AuthenticatedHomeserverGeneric.cs | 124 +++++++++++++++++++-- 1 file changed, 112 insertions(+), 12 deletions(-) (limited to 'LibMatrix/Homeservers') diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index 5db9a48..ef6fa68 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -12,19 +12,21 @@ using LibMatrix.Helpers; using LibMatrix.Responses; using LibMatrix.RoomTypes; using LibMatrix.Services; +using LibMatrix.Utilities; namespace LibMatrix.Homeservers; public class AuthenticatedHomeserverGeneric(string serverName, string accessToken) : RemoteHomeserver(serverName) { public static async Task Create(string serverName, string accessToken, string? proxy = null) where T : AuthenticatedHomeserverGeneric => await Create(typeof(T), serverName, accessToken, proxy) as T ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}"); + public static async Task Create(Type type, string serverName, string accessToken, string? proxy = null) { if (string.IsNullOrWhiteSpace(proxy)) proxy = null; - if(!type.IsAssignableTo(typeof(AuthenticatedHomeserverGeneric))) throw new ArgumentException("Type must be a subclass of AuthenticatedHomeserverGeneric", nameof(type)); + if (!type.IsAssignableTo(typeof(AuthenticatedHomeserverGeneric))) throw new ArgumentException("Type must be a subclass of AuthenticatedHomeserverGeneric", nameof(type)); var instance = Activator.CreateInstance(type, serverName, accessToken) as AuthenticatedHomeserverGeneric ?? throw new InvalidOperationException($"Failed to create instance of {type.Name}"); - + instance.ClientHttpClient = new() { Timeout = TimeSpan.FromMinutes(15), DefaultRequestHeaders = { @@ -44,7 +46,6 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke instance.WhoAmI = await instance.ClientHttpClient.GetFromJsonAsync("/_matrix/client/v3/account/whoami"); - return instance; } @@ -127,7 +128,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke } } - #region Utility Functions +#region Utility Functions public virtual async IAsyncEnumerable GetJoinedRoomsByType(string type) { var rooms = await GetJoinedRooms(); @@ -145,9 +146,9 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke } } - #endregion +#endregion - #region Account Data +#region Account Data public virtual async Task GetAccountDataAsync(string key) { // var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}"); @@ -168,7 +169,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke } } - #endregion +#endregion public async Task UpdateProfileAsync(UserProfileResponse? newProfile, bool preserveCustomRoomProfile = true) { if (newProfile is null) return; @@ -290,17 +291,116 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke return await res.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to join room?"); } - #region Room Profile Utility +#region Room Profile Utility private async Task> GetOwnRoomProfileWithIdAsync(GenericRoom room) { return new KeyValuePair(room.RoomId, await room.GetStateAsync("m.room.member", WhoAmI.UserId!)); } - #endregion - +#endregion + public async Task SetImpersonate(string mxid) { - if(ClientHttpClient.AdditionalQueryParameters.TryGetValue("user_id", out var existingMxid) && existingMxid == mxid && WhoAmI.UserId == mxid) return; + if (ClientHttpClient.AdditionalQueryParameters.TryGetValue("user_id", out var existingMxid) && existingMxid == mxid && WhoAmI.UserId == mxid) return; ClientHttpClient.AdditionalQueryParameters["user_id"] = mxid; WhoAmI = await ClientHttpClient.GetFromJsonAsync("/_matrix/client/v3/account/whoami"); } -} + + public async Task UploadFilterAsync(SyncFilter filter) { + var resp = await ClientHttpClient.PostAsJsonAsync("/_matrix/client/v3/user/" + UserId + "/filter", filter); + return await resp.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to upload filter?"); + } + + public async Task GetFilterAsync(string filterId) { + if (_filterCache.TryGetValue(filterId, out var filter)) return filter; + var resp = await ClientHttpClient.GetAsync("/_matrix/client/v3/user/" + UserId + "/filter/" + filterId); + return _filterCache[filterId] = await resp.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to get filter?"); + } + +#region Named filters + + private async Task?> GetNamedFilterListOrNullAsync(bool cached = true) { + if (cached && _namedFilterCache is not null) return _namedFilterCache; + try { + return _namedFilterCache = await GetAccountDataAsync>("gay.rory.libmatrix.named_filters"); + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_NOT_FOUND" }) throw; + } + + return null; + } + + /// + /// Utility function to allow avoiding serverside duplication + /// + /// Name of the filter (please properly namespace and possibly version this...) + /// The filter data + /// Filter ID response + /// + public async Task UploadNamedFilterAsync(string filterName, SyncFilter filter) { + var resp = await ClientHttpClient.PostAsJsonAsync("/_matrix/client/v3/user/" + UserId + "/filter", filter); + var idResp = await resp.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to upload filter?"); + + var filterList = await GetNamedFilterListOrNullAsync() ?? new(); + filterList[filterName] = idResp.FilterId; + await SetAccountDataAsync("gay.rory.libmatrix.named_filters", filterList); + + _namedFilterCache = filterList; + + return idResp; + } + + public async Task GetNamedFilterIdOrNullAsync(string filterName) { + var filterList = await GetNamedFilterListOrNullAsync() ?? new(); + return filterList.GetValueOrDefault(filterName); //todo: validate that filter exists + } + + public async Task GetNamedFilterOrNullAsync(string filterName) { + var filterId = await GetNamedFilterIdOrNullAsync(filterName); + if (filterId is null) return null; + return await GetFilterAsync(filterId); + } + + public async Task GetOrUploadNamedFilterIdAsync(string filterName, SyncFilter? filter = null) { + var filterId = await GetNamedFilterIdOrNullAsync(filterName); + if (filterId is not null) return filterId; + if (filter is null && CommonSyncFilters.FilterMap.TryGetValue(filterName, out var commonFilter)) filter = commonFilter; + if (filter is null) throw new ArgumentException($"Filter is null and no common filter was found, filterName={filterName}", nameof(filter)); + var idResp = await UploadNamedFilterAsync(filterName, filter); + return idResp.FilterId; + } + +#endregion + + public class FilterIdResponse { + [JsonPropertyName("filter_id")] + public required string FilterId { get; set; } + } + + public async Task> EnumerateAccountDataPerRoom(bool includeGlobal = false) { + var syncHelper = new SyncHelper(this); + syncHelper.FilterId = await GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetAccountDataWithRooms); + var resp = await syncHelper.SyncAsync(); + if(resp is null) throw new Exception("Sync failed"); + var perRoomAccountData = new Dictionary(); + + if(includeGlobal) + perRoomAccountData[""] = resp.AccountData; + foreach (var (roomId, room) in resp.Rooms?.Join ?? []) { + perRoomAccountData[roomId] = room.AccountData; + } + + return perRoomAccountData; + } + + public async Task EnumerateAccountData() { + var syncHelper = new SyncHelper(this); + syncHelper.FilterId = await GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetAccountData); + var resp = await syncHelper.SyncAsync(); + if(resp is null) throw new Exception("Sync failed"); + return resp.AccountData; + } + + private Dictionary? _namedFilterCache; + private Dictionary _filterCache = new(); +} \ No newline at end of file -- cgit 1.4.1