summary refs log tree commit diff
path: root/MxApiExtensions/Controllers
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2023-11-23 05:43:15 +0100
committerTheArcaneBrony <myrainbowdash949@gmail.com>2023-11-23 05:43:15 +0100
commitd3d95bbb271902391cbd43a11a6a5d72b0ccfaef (patch)
tree29fb4dd65d855a66fa65755fbf8d443a6154dc80 /MxApiExtensions/Controllers
parentFix sync (diff)
downloadMxApiExtensions-d3d95bbb271902391cbd43a11a6a5d72b0ccfaef.tar.xz
Add more code
Diffstat (limited to 'MxApiExtensions/Controllers')
-rw-r--r--MxApiExtensions/Controllers/Client/Room/RoomController.cs51
-rw-r--r--MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs71
-rw-r--r--MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs73
-rw-r--r--MxApiExtensions/Controllers/Client/SyncController.cs54
-rw-r--r--MxApiExtensions/Controllers/Extensions/DebugController.cs19
-rw-r--r--MxApiExtensions/Controllers/Other/GenericProxyController.cs19
-rw-r--r--MxApiExtensions/Controllers/Other/MediaProxyController.cs26
7 files changed, 205 insertions, 108 deletions
diff --git a/MxApiExtensions/Controllers/Client/Room/RoomController.cs b/MxApiExtensions/Controllers/Client/Room/RoomController.cs
new file mode 100644
index 0000000..a3e433d
--- /dev/null
+++ b/MxApiExtensions/Controllers/Client/Room/RoomController.cs
@@ -0,0 +1,51 @@
+using LibMatrix;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Services;
+
+namespace MxApiExtensions.Controllers.Client.Room;
+
+[ApiController]
+[Route("/")]
+public class RoomController(ILogger<LoginController> logger, HomeserverResolverService hsResolver, AuthenticationService auth, MxApiExtensionsConfiguration conf,
+        AuthenticatedHomeserverProviderService hsProvider)
+    : ControllerBase {
+    [HttpGet("/_matrix/client/{_}/rooms/{roomId}/members_by_homeserver")]
+    public async Task<Dictionary<string, List<string>>> GetRoomMembersByHomeserver(string _, [FromRoute] string roomId, [FromQuery] bool joinedOnly = true) {
+        var hs = await hsProvider.GetHomeserver();
+        var room = hs.GetRoom(roomId);
+        return await room.GetMembersByHomeserverAsync(joinedOnly);
+    }
+
+    /// <summary>
+    /// Fetches up to <paramref name="limit"/> timeline events
+    /// </summary>
+    /// <param name="_"></param>
+    /// <param name="roomId"></param>
+    /// <param name="from"></param>
+    /// <param name="limit"></param>
+    /// <param name="dir"></param>
+    /// <param name="filter"></param>
+    /// <param name="includeState"></param>
+    /// <param name="fixForward">Reverse load all messages and reverse on API side, fixes history starting at join event</param>
+    /// <returns></returns>
+    [HttpGet("/_matrix/client/{_}/rooms/{roomId}/mass_messages")]
+    public async IAsyncEnumerable<StateEventResponse> RedactUser(string _, [FromRoute] string roomId, [FromQuery(Name = "from")] string from = "",
+        [FromQuery(Name = "limit")] int limit = 100, [FromQuery(Name = "dir")] string dir = "b", [FromQuery(Name = "filter")] string filter = "",
+        [FromQuery(Name = "include_state")] bool includeState = true, [FromQuery(Name = "fix_forward")] bool fixForward = false) {
+        var hs = await hsProvider.GetHomeserver();
+        var room = hs.GetRoom(roomId);
+        var msgs = room.GetManyMessagesAsync(from: from, limit: limit, dir: dir, filter: filter, includeState: includeState, fixForward: fixForward);
+        await foreach (var resp in msgs) {
+            Console.WriteLine($"GetMany messages returned {resp.Chunk.Count} timeline events and {resp.State.Count} state events, end={resp.End}");
+            foreach (var timelineEvent in resp.Chunk) {
+                yield return timelineEvent;
+            }
+
+            if (includeState)
+                foreach (var timelineEvent in resp.State) {
+                    yield return timelineEvent;
+                }
+        }
+    }
+}
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs b/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs
index b756582..e882c8a 100644
--- a/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs
+++ b/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs
@@ -1,18 +1,73 @@
+using System.Buffers.Text;
+using System.Net.Http.Headers;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
 using LibMatrix.Services;
 using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Classes;
+using MxApiExtensions.Classes.LibMatrix;
+using MxApiExtensions.Extensions;
 using MxApiExtensions.Services;
 
-namespace MxApiExtensions.Controllers.Client.Room;
+namespace MxApiExtensions.Controllers;
 
 [ApiController]
 [Route("/")]
-public class RoomController(ILogger<LoginController> logger, HomeserverResolverService hsResolver, AuthenticationService auth, MxApiExtensionsConfiguration conf,
-        AuthenticatedHomeserverProviderService hsProvider)
+public class RoomsSendMessageController(ILogger<LoginController> logger, UserContextService userContextService)
     : ControllerBase {
-    [HttpGet("/_matrix/client/{_}/rooms/{roomId}/members_by_homeserver")]
-    public async Task<Dictionary<string, List<string>>> GetRoomMembersByHomeserver(string _, [FromRoute] string roomId, [FromQuery] bool joinedOnly = true) {
-        var hs = await hsProvider.GetHomeserver();
-        var room = hs.GetRoom(roomId);
-        return await room.GetMembersByHomeserverAsync(joinedOnly);
+    [HttpPut("/_matrix/client/{_}/rooms/{roomId}/send/m.room.message/{txnId}")]
+    public async Task Proxy([FromBody] JsonObject request, [FromRoute] string roomId, [FromRoute] string txnId, string _) {
+        var uc = await userContextService.GetCurrentUserContext();
+        // var hs = await hsProvider.GetHomeserver();
+
+        var msg = request.Deserialize<RoomMessageEventContent>();
+        if (msg is not null && msg.Body.StartsWith("mxae!")) {
+#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+            handleMxaeCommand(uc, roomId, msg);
+#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+            await Response.WriteAsJsonAsync(new EventIdResponse() {
+                EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100))
+            });
+            await Response.CompleteAsync();
+        }
+        else {
+            try {
+                var resp = await uc.Homeserver.ClientHttpClient.PutAsJsonAsync($"{Request.Path}{Request.QueryString}", request);
+                await Response.WriteHttpResponse(resp);
+                // var loginResp = await resp.Content.ReadAsStringAsync();
+                // Response.StatusCode = (int)resp.StatusCode;
+                // Response.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json";
+                // await Response.StartAsync();
+                // await Response.WriteAsync(loginResp);
+                // await Response.CompleteAsync();
+            }
+            catch (MatrixException e) {
+                await Response.StartAsync();
+                await Response.WriteAsync(e.GetAsJson());
+                await Response.CompleteAsync();
+            }
+        }
+    }
+
+    private async Task handleMxaeCommand(UserContextService.UserContext hs, string roomId, RoomMessageEventContent msg) {
+        if (hs.SyncState is null) return;
+        hs.SyncState.SendEphemeralTimelineEventInRoom(roomId, new() {
+            Sender = "@mxae:" + Request.Host.Value,
+            Type = "m.room.message",
+            TypedContent = MessageFormatter.FormatSuccess("Thinking..."),
+            OriginServerTs = (ulong)new DateTimeOffset(DateTime.UtcNow.ToUniversalTime()).ToUnixTimeMilliseconds(),
+            Unsigned = new() {
+                Age = 1
+            },
+            RoomId = roomId,
+            EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100))
+        });
     }
 }
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs b/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs
deleted file mode 100644
index e882c8a..0000000
--- a/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System.Buffers.Text;
-using System.Net.Http.Headers;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using ArcaneLibs.Extensions;
-using LibMatrix;
-using LibMatrix.EventTypes.Spec;
-using LibMatrix.Extensions;
-using LibMatrix.Helpers;
-using LibMatrix.Homeservers;
-using LibMatrix.Responses;
-using LibMatrix.Services;
-using Microsoft.AspNetCore.Mvc;
-using MxApiExtensions.Classes;
-using MxApiExtensions.Classes.LibMatrix;
-using MxApiExtensions.Extensions;
-using MxApiExtensions.Services;
-
-namespace MxApiExtensions.Controllers;
-
-[ApiController]
-[Route("/")]
-public class RoomsSendMessageController(ILogger<LoginController> logger, UserContextService userContextService)
-    : ControllerBase {
-    [HttpPut("/_matrix/client/{_}/rooms/{roomId}/send/m.room.message/{txnId}")]
-    public async Task Proxy([FromBody] JsonObject request, [FromRoute] string roomId, [FromRoute] string txnId, string _) {
-        var uc = await userContextService.GetCurrentUserContext();
-        // var hs = await hsProvider.GetHomeserver();
-
-        var msg = request.Deserialize<RoomMessageEventContent>();
-        if (msg is not null && msg.Body.StartsWith("mxae!")) {
-#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-            handleMxaeCommand(uc, roomId, msg);
-#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-            await Response.WriteAsJsonAsync(new EventIdResponse() {
-                EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100))
-            });
-            await Response.CompleteAsync();
-        }
-        else {
-            try {
-                var resp = await uc.Homeserver.ClientHttpClient.PutAsJsonAsync($"{Request.Path}{Request.QueryString}", request);
-                await Response.WriteHttpResponse(resp);
-                // var loginResp = await resp.Content.ReadAsStringAsync();
-                // Response.StatusCode = (int)resp.StatusCode;
-                // Response.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json";
-                // await Response.StartAsync();
-                // await Response.WriteAsync(loginResp);
-                // await Response.CompleteAsync();
-            }
-            catch (MatrixException e) {
-                await Response.StartAsync();
-                await Response.WriteAsync(e.GetAsJson());
-                await Response.CompleteAsync();
-            }
-        }
-    }
-
-    private async Task handleMxaeCommand(UserContextService.UserContext hs, string roomId, RoomMessageEventContent msg) {
-        if (hs.SyncState is null) return;
-        hs.SyncState.SendEphemeralTimelineEventInRoom(roomId, new() {
-            Sender = "@mxae:" + Request.Host.Value,
-            Type = "m.room.message",
-            TypedContent = MessageFormatter.FormatSuccess("Thinking..."),
-            OriginServerTs = (ulong)new DateTimeOffset(DateTime.UtcNow.ToUniversalTime()).ToUnixTimeMilliseconds(),
-            Unsigned = new() {
-                Age = 1
-            },
-            RoomId = roomId,
-            EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100))
-        });
-    }
-}
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Client/SyncController.cs b/MxApiExtensions/Controllers/Client/SyncController.cs
index 7f9ed1d..615502b 100644
--- a/MxApiExtensions/Controllers/Client/SyncController.cs
+++ b/MxApiExtensions/Controllers/Client/SyncController.cs
@@ -28,12 +28,14 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
     private UserContextService.UserContext userContext;
     private Stopwatch _syncElapsed = Stopwatch.StartNew();
     private static SemaphoreSlim _semaphoreSlim = new(1, 1);
+    public static List<Task> TrackedTasks { get; set; } = new();
 
     [HttpGet("/_matrix/client/{_}/sync")]
     public async Task Sync(string _, [FromQuery] string? since, [FromQuery] int timeout = 1000) {
         // temporary variables
         bool startedNewTask = false;
         Task? preloadTask = null;
+        TrackedTasks.RemoveAll(x => x.Status == TaskStatus.RanToCompletion);
 
         // get user context based on authentication
         userContext = await userContextService.GetCurrentUserContext();
@@ -50,12 +52,12 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
 
         //prevent duplicate initialisation
         await _semaphoreSlim.WaitAsync();
-        
+
         //if we don't have a sync state for this user...
         if (userContext.SyncState is null) {
             logger.LogInformation("Started tracking sync state for {} on {} ({})", userContext.Homeserver.WhoAmI.UserId, userContext.Homeserver.ServerName,
                 userContext.Homeserver.AccessToken);
-            
+
             //create a new sync state
             userContext.SyncState = new SyncState {
                 Homeserver = userContext.Homeserver,
@@ -68,7 +70,7 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
                 })
             };
             startedNewTask = true;
-            
+
             //if this is an initial sync, and the user has enabled this, preload data
             if (string.IsNullOrWhiteSpace(since) && userContext.UserConfiguration.InitialSyncPreload.Enable) {
                 logger.LogInformation("Sync data preload for {} on {} ({}) starting", userContext.Homeserver.WhoAmI.UserId, userContext.Homeserver.ServerName,
@@ -102,11 +104,13 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
 
         //await scope-local tasks in order to prevent disposal
         if (preloadTask is not null) {
+            TrackedTasks.Add(preloadTask);
             await preloadTask;
             preloadTask.Dispose();
         }
 
         if (startedNewTask && userContext.SyncState?.NextSyncResponse is not null) {
+            TrackedTasks.Add(userContext.SyncState.NextSyncResponse);
             var resp = await userContext.SyncState.NextSyncResponse;
             var sr = await resp.Content.ReadFromJsonAsync<JsonObject>();
             if (sr!.ContainsKey("error")) throw sr.Deserialize<MatrixException>()!;
@@ -123,7 +127,7 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
         do {
             if (userContext.SyncState is null) throw new NullReferenceException("syncState is null!");
             // if (userContext.SyncState.NextSyncResponse is null) throw new NullReferenceException("NextSyncResponse is null");
-            
+
             //check if upstream has responded, if so, return upstream response
             // if (userContext.SyncState.NextSyncResponse is { IsCompleted: true } syncResponse) {
             //     var resp = await syncResponse;
@@ -146,7 +150,7 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
             }
 
             // await Task.Delay(Math.Clamp(timeout, 25, 250)); //wait 25-250ms between checks
-            await Task.Delay(Math.Clamp(userContextService.SessionCount * 10 ,25, 500));
+            await Task.Delay(Math.Clamp(userContextService.SessionCount * 10, 25, 500));
         } while (_syncElapsed.ElapsedMilliseconds < timeout + 500); //... while we haven't gone >500ms over expected timeout
 
         //we didn't get a response, send a bogus response
@@ -155,10 +159,11 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
             new());
     }
 
-
-    private async Task EnqueuePreloadData(SyncState syncState) {
-        await EnqueuePreloadAccountData(syncState);
-        await EnqueuePreloadRooms(syncState);
+    private static async Task EnqueuePreloadData(SyncState syncState) {
+        new Thread(async () => {
+            await EnqueuePreloadAccountData(syncState);
+            await EnqueuePreloadRooms(syncState);
+        }).Start();
     }
 
     private static List<string> CommonAccountDataKeys = new() {
@@ -179,8 +184,9 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
         "m.secret_storage.default_key",
         "gay.rory.mxapiextensions.userconfig"
     };
+
     //enqueue common account data
-    private async Task EnqueuePreloadAccountData(SyncState syncState) {
+    private static async Task EnqueuePreloadAccountData(SyncState syncState) {
         var syncMsg = new SyncResponse() {
             AccountData = new() {
                 Events = new()
@@ -193,22 +199,23 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
                     RawContent = await syncState.Homeserver.GetAccountDataAsync<JsonObject>(key)
                 });
             }
-            catch {}
+            catch { }
         }
+
         syncState.SyncQueue.Enqueue(syncMsg);
     }
 
-    private async Task EnqueuePreloadRooms(SyncState syncState) {
+    private static async Task EnqueuePreloadRooms(SyncState syncState) {
         //get the users's rooms
         var rooms = await syncState.Homeserver.GetJoinedRooms();
-        
+
         //get the user's DM rooms
         var mDirectContent = await syncState.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct");
         var dmRooms = mDirectContent.SelectMany(pair => pair.Value);
 
         //get our own homeserver's server_name
         var ownHs = syncState.Homeserver.WhoAmI!.UserId!.Split(':')[1];
-        
+
         //order rooms by expected state size, since large rooms take a long time to return
         rooms = rooms.OrderBy(x => {
             if (dmRooms.Contains(x.RoomId)) return -1;
@@ -217,20 +224,25 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
             if (HomeserverWeightEstimation.EstimatedSize.ContainsKey(parts[1])) return HomeserverWeightEstimation.EstimatedSize[parts[1]] + parts[0].Length;
             return 5000;
         }).ToList();
-        
+
+        foreach (var room in rooms) {
+            new Thread(async () => await EnqueueRoomData(syncState, room)).Start();
+        }
+
         //start all fetch tasks
-        var roomDataTasks = rooms.Select(room => EnqueueRoomData(syncState, room)).ToList();
-        logger.LogInformation("Preloading data for {} rooms on {} ({})", roomDataTasks.Count, syncState.Homeserver.ServerName, syncState.Homeserver.AccessToken);
+        // var roomDataTasks = rooms.Select(room => EnqueueRoomData(syncState, room)).ToList();
+        // logger.LogInformation("Preloading data for {} rooms on {} ({})", roomDataTasks.Count, syncState.Homeserver.ServerName, syncState.Homeserver.AccessToken);
 
         //wait for all of them to finish
-        await Task.WhenAll(roomDataTasks);
+        // TrackedTasks.AddRange(roomDataTasks);
+        // await Task.WhenAll(roomDataTasks);
     }
 
     private static readonly SemaphoreSlim _roomDataSemaphore = new(4, 4);
 
-    private async Task EnqueueRoomData(SyncState syncState, GenericRoom room) {
+    private static async Task EnqueueRoomData(SyncState syncState, GenericRoom room) {
         //limit concurrent requests, to not overload upstream
-        await _roomDataSemaphore.WaitAsync();
+        // await _roomDataSemaphore.WaitAsync();
         //get the room's state
         var roomState = room.GetFullStateAsync();
         //get the room's timeline, reversed 
@@ -277,6 +289,6 @@ public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfi
 
         //finally, actually put the response in queue
         syncState.SyncQueue.Enqueue(syncResponse);
-        _roomDataSemaphore.Release();
+        // _roomDataSemaphore.Release();
     }
 }
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Extensions/DebugController.cs b/MxApiExtensions/Controllers/Extensions/DebugController.cs
index ae9ecc5..0a54481 100644
--- a/MxApiExtensions/Controllers/Extensions/DebugController.cs
+++ b/MxApiExtensions/Controllers/Extensions/DebugController.cs
@@ -1,5 +1,6 @@
 using System.Collections.Concurrent;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.OpenApi.Extensions;
 using MxApiExtensions.Classes.LibMatrix;
 using MxApiExtensions.Services;
 
@@ -15,6 +16,7 @@ public class DebugController(ILogger<ProxyConfigurationController> logger, MxApi
 
     [HttpGet("debug")]
     public async Task<object?> GetDebug() {
+#if !DEBUG
         var user = await userContextService.GetCurrentUserContext();
         var mxid = user.Homeserver.UserId;
         if(!config.Admins.Contains(mxid)) {
@@ -29,8 +31,19 @@ public class DebugController(ILogger<ProxyConfigurationController> logger, MxApi
             await Response.CompleteAsync();
             return null;
         }
-
         _logger.LogInformation("Got debug request for {user}", mxid);
-        return UserContextService.UserContextStore;
+#endif
+
+        return new {
+            syncControllerTasks = SyncController.TrackedTasks.Select(t => new {
+                t?.Id,
+                t?.IsCompleted,
+                t?.IsCompletedSuccessfully,
+                t?.IsCanceled,
+                t?.IsFaulted,
+                Status = t?.Status.GetDisplayName()
+            }),
+            UserContextService.UserContextStore
+        };
     }
-}
+}
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Other/GenericProxyController.cs b/MxApiExtensions/Controllers/Other/GenericProxyController.cs
index bae07c0..36ceab7 100644
--- a/MxApiExtensions/Controllers/Other/GenericProxyController.cs
+++ b/MxApiExtensions/Controllers/Other/GenericProxyController.cs
@@ -6,10 +6,15 @@ using MxApiExtensions.Services;
 namespace MxApiExtensions.Controllers;
 
 [ApiController]
-[Route("/{*_}")]
+[Route("/_matrix/{*_}")]
 public class GenericController(ILogger<GenericController> logger, MxApiExtensionsConfiguration config, AuthenticationService authenticationService,
         AuthenticatedHomeserverProviderService authenticatedHomeserverProviderService)
     : ControllerBase {
+    /// <summary>
+    /// Direct proxy to upstream
+    /// </summary>
+    /// <param name="_">API path (unused, as Request.Path is used instead)</param>
+    /// <param name="access_token">Optional access token</param>
     [HttpGet]
     public async Task Proxy([FromQuery] string? access_token, string? _) {
         try {
@@ -60,6 +65,11 @@ public class GenericController(ILogger<GenericController> logger, MxApiExtension
         }
     }
 
+    /// <summary>
+    /// Direct proxy to upstream
+    /// </summary>
+    /// <param name="_">API path (unused, as Request.Path is used instead)</param>
+    /// <param name="access_token">Optional access token</param>
     [HttpPost]
     public async Task ProxyPost([FromQuery] string? access_token, string _) {
         try {
@@ -117,6 +127,11 @@ public class GenericController(ILogger<GenericController> logger, MxApiExtension
         }
     }
 
+    /// <summary>
+    /// Direct proxy to upstream
+    /// </summary>
+    /// <param name="_">API path (unused, as Request.Path is used instead)</param>
+    /// <param name="access_token">Optional access token</param>
     [HttpPut]
     public async Task ProxyPut([FromQuery] string? access_token, string _) {
         try {
@@ -173,4 +188,4 @@ public class GenericController(ILogger<GenericController> logger, MxApiExtension
             await Response.CompleteAsync();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/MxApiExtensions/Controllers/Other/MediaProxyController.cs b/MxApiExtensions/Controllers/Other/MediaProxyController.cs
index fb40aa2..d4c4ea0 100644
--- a/MxApiExtensions/Controllers/Other/MediaProxyController.cs
+++ b/MxApiExtensions/Controllers/Other/MediaProxyController.cs
@@ -36,25 +36,49 @@ public class MediaProxyController(ILogger<GenericController> logger, MxApiExtens
                     var a = await authenticatedHomeserverProviderService.TryGetRemoteHomeserver();
                     if(a is not null)
                         FeasibleHomeservers.Add(a);
+
+                    if (a is AuthenticatedHomeserverGeneric ahg) {
+                        var rooms = await ahg.GetJoinedRooms();
+                        foreach (var room in rooms) {
+                            var ahs = (await room.GetMembersByHomeserverAsync()).Keys.Select(x=>x.ToString()).ToList();
+                            foreach (var ah in ahs) {
+                                try {
+                                    if (!FeasibleHomeservers.Any(x => x.BaseUrl == ah)) {
+                                        FeasibleHomeservers.Add(await hsProvider.GetRemoteHomeserver(ah));
+                                    }
+                                }
+                                catch { }
+                            }
+                        }
+                    }
                 }
                 
                 FeasibleHomeservers.Add(await hsProvider.GetRemoteHomeserver(serverName));
                 
+                
                 foreach (var homeserver in FeasibleHomeservers) {
                     var resp = await homeserver.ClientHttpClient.GetAsync($"{Request.Path}");
                     if(!resp.IsSuccessStatusCode) continue;
                     entry.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json";
                     entry.Data = await resp.Content.ReadAsByteArrayAsync();
+                    if (entry.Data is not { Length: >0 }) throw new NullReferenceException("No data received?");
                     break;
                 }
+                if (entry.Data is not { Length: >0 }) throw new NullReferenceException("No data received from any homeserver?");
+            }
+            else if (_mediaCache[$"{serverName}/{mediaId}"].Data is not { Length: > 0 }) {
+                _mediaCache.Remove($"{serverName}/{mediaId}");
+                await ProxyMedia(_, serverName, mediaId);
+                return;
             }
             else entry = _mediaCache[$"{serverName}/{mediaId}"];
+            if (entry.Data is null) throw new NullReferenceException("No data?");
             _semaphore.Release();
             
             Response.StatusCode = 200;
             Response.ContentType = entry.ContentType;
             await Response.StartAsync();
-            await Response.Body.WriteAsync(entry.Data, 0, entry.Data.Length);
+            await Response.Body.WriteAsync(entry.Data.ToArray(), 0, entry.Data.Length);
             await Response.Body.FlushAsync();
             await Response.CompleteAsync();
         }