diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/EventIdResponse.cs
index c2ad273..0a7cfd9 100644
--- a/LibMatrix/EventIdResponse.cs
+++ b/LibMatrix/EventIdResponse.cs
@@ -3,8 +3,6 @@ using System.Text.Json.Serialization;
namespace LibMatrix;
public class EventIdResponse(string eventId) {
- public EventIdResponse(StateEventResponse stateEventResponse) : this(stateEventResponse.EventId ?? throw new NullReferenceException("State event ID is null!")) { }
-
[JsonPropertyName("event_id")]
public string EventId { get; set; } = eventId;
}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index 68f6300..07953e3 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -50,11 +50,18 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
Content.FormattedBody += "</font>";
return this;
}
+
+ public MessageBuilder WithCustomEmoji(string mxcUri, string name) {
+ Content.Body += $"{{{name}}}";
+ Content.FormattedBody += $"<img data-mx-emoticon height=\"32\" src=\"{mxcUri}\" alt=\"{name}\" title=\"{name}\" />";
+ return this;
+ }
+
+ public MessageBuilder WithRainbowString(string text, byte skip = 1, int offset = 0, double lengthFactor = 255.0, bool useLength = true) {
+ if (useLength) {
+ lengthFactor = text.Length;
+ }
- public MessageBuilder WithRainbowString(string text, byte skip = 1, int offset = 0, double lengthFactor = 255.0, bool useLength = true) =>
- // if (useLength) {
- // lengthFactor = text.Length;
- // }
// HslaColorInterpolator interpolator = new((0, 255, 128, 255), (255, 255, 128, 255));
// // RainbowEnumerator enumerator = new(skip, offset, lengthFactor);
// for (int i = 0; i < text.Length; i++) {
@@ -63,5 +70,12 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
// // Console.WriteLine($"RBA: {r} {g} {b} {a}");
// // Content.FormattedBody += $"<font color=\"#{r:X2}{g:X2}{b:X2}\">{text[i]}</font>";
// }
- this;
+ return this;
+ }
+
+ public MessageBuilder WithCodeBlock(string code, string language = "plaintext") {
+ Content.Body += code;
+ Content.FormattedBody += $"<pre><code class=\"language-{language}\">{code}</code></pre>";
+ return this;
+ }
}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 9d339e4..e696b70 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -55,7 +55,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
private async Task updateFilterAsync() {
if (!string.IsNullOrWhiteSpace(NamedFilterName)) {
- _filterId = await homeserver.GetOrUploadNamedFilterIdAsync(NamedFilterName);
+ _filterId = await homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(NamedFilterName);
if (_filterId is null)
if (logger is null) Console.WriteLine($"Failed to get filter ID for named filter {NamedFilterName}");
else logger.LogWarning("Failed to get filter ID for named filter {NamedFilterName}", NamedFilterName);
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 1c93235..b4c1cc9 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Net.Http.Json;
+using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -10,6 +11,7 @@ using LibMatrix.EventTypes.Spec.State;
using LibMatrix.Extensions;
using LibMatrix.Filters;
using LibMatrix.Helpers;
+using LibMatrix.Homeservers.Extensions.NamedCaches;
using LibMatrix.Responses;
using LibMatrix.RoomTypes;
using LibMatrix.Services;
@@ -46,6 +48,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
}
instance.WhoAmI = await instance.ClientHttpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami");
+ instance.NamedCaches = new HsNamedCaches(instance);
return instance;
}
@@ -57,6 +60,8 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
public string AccessToken { get; set; } = accessToken;
+ public HsNamedCaches NamedCaches { get; set; } = null!;
+
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);
@@ -294,6 +299,12 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
WhoAmI = await ClientHttpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami");
}
+ /// <summary>
+ /// Upload a filter to the homeserver. Substitutes @me with the user's ID.
+ /// </summary>
+ /// <param name="filter"></param>
+ /// <returns></returns>
+ /// <exception cref="Exception"></exception>
public async Task<FilterIdResponse> UploadFilterAsync(SyncFilter filter) {
List<List<string>?> senderLists = [
filter.AccountData?.Senders,
@@ -326,69 +337,21 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
return _filterCache[filterId] = await resp.Content.ReadFromJsonAsync<SyncFilter>() ?? throw new Exception("Failed to get filter?");
}
-#region Named filters
-
- private async Task<Dictionary<string, string>?> GetNamedFilterListOrNullAsync(bool cached = true) {
- if (cached && _namedFilterCache is not null) return _namedFilterCache;
- try {
- return _namedFilterCache = await GetAccountDataAsync<Dictionary<string, string>>("gay.rory.libmatrix.named_filters");
- }
- catch (MatrixException e) {
- if (e is not { ErrorCode: "M_NOT_FOUND" }) throw;
- }
-
- return null;
- }
-
- /// <summary>
- /// Utility function to allow avoiding serverside duplication
- /// </summary>
- /// <param name="filterName">Name of the filter (<i>please</i> properly namespace and possibly version this...)</param>
- /// <param name="filter">The filter data</param>
- /// <returns>Filter ID response</returns>
- /// <exception cref="Exception"></exception>
- public async Task<FilterIdResponse> UploadNamedFilterAsync(string filterName, SyncFilter filter) {
- var idResp = await UploadFilterAsync(filter);
-
- var filterList = await GetNamedFilterListOrNullAsync() ?? new Dictionary<string, string>();
- filterList[filterName] = idResp.FilterId;
- await SetAccountDataAsync("gay.rory.libmatrix.named_filters", filterList);
-
- _namedFilterCache = filterList;
-
- return idResp;
- }
-
- public async Task<string?> GetNamedFilterIdOrNullAsync(string filterName) {
- var filterList = await GetNamedFilterListOrNullAsync() ?? new Dictionary<string, string>();
- return filterList.GetValueOrDefault(filterName); //todo: validate that filter exists
- }
-
- public async Task<SyncFilter?> GetNamedFilterOrNullAsync(string filterName) {
- var filterId = await GetNamedFilterIdOrNullAsync(filterName);
- if (filterId is null) return null;
- return await GetFilterAsync(filterId);
- }
-
- public async Task<string?> 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; }
}
+ /// <summary>
+ /// Enumerate all account data per room.
+ /// <b>Warning</b>: This uses /sync!
+ /// </summary>
+ /// <param name="includeGlobal">Include non-room account data</param>
+ /// <returns>Dictionary of room IDs and their account data.</returns>
+ /// <exception cref="Exception"></exception>
public async Task<Dictionary<string, EventList?>> EnumerateAccountDataPerRoom(bool includeGlobal = false) {
var syncHelper = new SyncHelper(this);
- syncHelper.FilterId = await GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetAccountDataWithRooms);
+ syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountDataWithRooms);
var resp = await syncHelper.SyncAsync();
if (resp is null) throw new Exception("Sync failed");
var perRoomAccountData = new Dictionary<string, EventList?>();
@@ -400,9 +363,15 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
return perRoomAccountData;
}
+ /// <summary>
+ /// Enumerate all non-room account data.
+ /// <b>Warning</b>: This uses /sync!
+ /// </summary>
+ /// <returns>All account data.</returns>
+ /// <exception cref="Exception"></exception>
public async Task<EventList?> EnumerateAccountData() {
var syncHelper = new SyncHelper(this);
- syncHelper.FilterId = await GetOrUploadNamedFilterIdAsync(CommonSyncFilters.GetAccountData);
+ syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData);
var resp = await syncHelper.SyncAsync();
if (resp is null) throw new Exception("Sync failed");
return resp.AccountData;
@@ -420,4 +389,14 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
return await res.Content.ReadFromJsonAsync<JsonObject>();
}
+
+ public class HsNamedCaches {
+ internal HsNamedCaches(AuthenticatedHomeserverGeneric hs) {
+ FileCache = new NamedFileCache(hs);
+ FilterCache = new NamedFilterCache(hs);
+ }
+
+ public NamedFilterCache FilterCache { get; init; }
+ public NamedFileCache FileCache { get; init; }
+ }
}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
new file mode 100644
index 0000000..622eef6
--- /dev/null
+++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
@@ -0,0 +1,37 @@
+namespace LibMatrix.Homeservers.Extensions.NamedCaches;
+
+public class NamedCache<T>(AuthenticatedHomeserverGeneric hs, string name) where T : class {
+ private Dictionary<string, T>? _cache = new();
+ private DateTime _expiry = DateTime.MinValue;
+
+ public async Task<Dictionary<string, T>> ReadCacheMapAsync() {
+ _cache = await hs.GetAccountDataOrNullAsync<Dictionary<string, T>>(name);
+
+ return _cache ?? new();
+ }
+
+ public async Task<Dictionary<string,T>> ReadCacheMapCachedAsync() {
+ if (_expiry < DateTime.Now || _cache == null) {
+ _cache = await ReadCacheMapAsync();
+ _expiry = DateTime.Now.AddMinutes(5);
+ }
+
+ return _cache;
+ }
+
+ public virtual async Task<T?> GetValueAsync(string key) {
+ return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key);
+ }
+
+ public virtual async Task<T> SetValueAsync(string key, T value) {
+ var cache = await ReadCacheMapCachedAsync();
+ cache[key] = value;
+ await hs.SetAccountDataAsync(name, cache);
+
+ return value;
+ }
+
+ public virtual async Task<T> GetOrSetValueAsync(string key, Func<Task<T>> value) {
+ return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key) ?? await SetValueAsync(key, await value());
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFileCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFileCache.cs
new file mode 100644
index 0000000..87b7636
--- /dev/null
+++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFileCache.cs
@@ -0,0 +1,3 @@
+namespace LibMatrix.Homeservers.Extensions.NamedCaches;
+
+public class NamedFileCache(AuthenticatedHomeserverGeneric hs) : NamedCache<string>(hs, "gay.rory.libmatrix.named_cache.media") { }
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
new file mode 100644
index 0000000..76533a4
--- /dev/null
+++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
@@ -0,0 +1,33 @@
+using LibMatrix.Filters;
+using LibMatrix.Utilities;
+
+namespace LibMatrix.Homeservers.Extensions.NamedCaches;
+
+public class NamedFilterCache(AuthenticatedHomeserverGeneric hs) : NamedCache<string>(hs, "gay.rory.libmatrix.named_cache.filter") {
+ /// <summary>
+ /// <inheritdoc cref="NamedCache{T}.GetOrSetValueAsync"/>
+ /// Allows passing a filter directly, or using a common filter.
+ /// Substitutes @me for the user's ID.
+ /// </summary>
+ /// <param name="key">Filter name</param>
+ /// <param name="filter">Filter to upload if not cached, otherwise defaults to common filters if that exists.</param>
+ /// <returns></returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public async Task<string> GetOrSetValueAsync(string key, SyncFilter? filter = null) {
+ var existingValue = await GetValueAsync(key);
+ if (existingValue != null) {
+ return existingValue;
+ }
+
+ if (filter is null) {
+ if(CommonSyncFilters.FilterMap.TryGetValue(key, out var commonFilter)) {
+ filter = commonFilter;
+ } else {
+ throw new ArgumentNullException(nameof(filter));
+ }
+ }
+
+ var filterUpload = await hs.UploadFilterAsync(filter);
+ return await SetValueAsync(key, filterUpload.FilterId);
+ }
+}
\ No newline at end of file
|