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
|