about summary refs log tree commit diff
path: root/Utilities
diff options
context:
space:
mode:
Diffstat (limited to 'Utilities')
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs76
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs33
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs19
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs5
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs42
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs20
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs2
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs31
8 files changed, 211 insertions, 17 deletions
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
index 66548e2..5550c26 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
@@ -1,3 +1,4 @@
+using System.Security.Cryptography;
 using System.Text.Json.Nodes;
 using LibMatrix.HomeserverEmulator.Services;
 using LibMatrix.Responses;
@@ -8,7 +9,7 @@ namespace LibMatrix.HomeserverEmulator.Controllers;
 
 [ApiController]
 [Route("/_matrix/client/{version}/")]
-public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService) : ControllerBase {
+public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService, HSEConfiguration config) : ControllerBase {
     [HttpPost("login")]
     public async Task<LoginResponse> Login(LoginRequest request) {
         if (!request.Identifier.User.StartsWith('@'))
@@ -58,6 +59,79 @@ public class AuthController(ILogger<AuthController> logger, UserStore userStore,
         user.AccessTokens.Remove(token);
         return new { };
     }
+
+    [HttpPost("register")]
+    public async Task<object> Register(JsonObject request, [FromQuery] string kind = "user") {
+        if (kind == "guest") {
+            var user = await userStore.CreateUser(Random.Shared.NextInt64(long.MaxValue).ToString(), kind: "guest");
+            return user.Login();
+        }
+
+        if (request.Count == 0) {
+            return new {
+                session = Guid.NewGuid().ToString(),
+                flows = new {
+                    stages = new[] {
+                        "m.login.dummy",
+                    }
+                }
+            };
+        }
+
+        if (request.ContainsKey("password")) {
+            var parts = request["username"].ToString().Split(':');
+            var localpart = parts[0].TrimStart('@');
+            var user = await userStore.CreateUser($"@{localpart}:{config.ServerName}");
+            var login = user.Login();
+
+            if (request.ContainsKey("initial_device_display_name"))
+                user.AccessTokens[login.AccessToken].DeviceName = request["initial_device_display_name"]!.ToString();
+
+            return login;
+        }
+
+        return new { };
+    }
+
+    [HttpGet("register/available")]
+    public async Task<object> IsUsernameAvailable([FromQuery] string username) {
+        return new {
+            available = await userStore.GetUserById($"@{username}:{config.ServerName}") is null
+        };
+    }
+    
+    // [HttpPost("account/deactivate")]
+    // public async Task<object> DeactivateAccount() {
+    //     var token = tokenService.GetAccessToken(HttpContext);
+    //     var user = await userStore.GetUserByToken(token);
+    //     if (user == null)
+    //         throw new MatrixException() {
+    //             ErrorCode = "M_UNKNOWN_TOKEN",
+    //             Error = "No such user"
+    //         };
+    //
+    //     
+    //     return new { };
+    // }
+    
+    #region 3PID
+    
+    [HttpGet("account/3pid")]
+    public async Task<object> Get3pid() {
+        var token = tokenService.GetAccessToken(HttpContext);
+        var user = await userStore.GetUserByToken(token);
+        if (user == null)
+            throw new MatrixException() {
+                ErrorCode = "M_UNKNOWN_TOKEN",
+                Error = "No such user"
+            };
+
+        return new {
+            threepids = (object[])[]
+        };
+    }
+    
+    #endregion
 }
 
 public class LoginFlowsResponse {
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
index 52d5932..b29edf5 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
@@ -1,5 +1,6 @@
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.HomeserverEmulator.Services;
 using LibMatrix.Homeservers;
@@ -12,6 +13,8 @@ namespace LibMatrix.HomeserverEmulator.Controllers;
 [ApiController]
 [Route("/_matrix/")]
 public class DirectoryController(ILogger<DirectoryController> logger, RoomStore roomStore) : ControllerBase {
+#region Room directory
+
     [HttpGet("client/v3/directory/room/{alias}")]
     public async Task<AliasResult> GetRoomAliasV3(string alias) {
         var match = roomStore._rooms.FirstOrDefault(x =>
@@ -31,4 +34,34 @@ public class DirectoryController(ILogger<DirectoryController> logger, RoomStore
             Servers = servers
         };
     }
+
+#endregion
+
+#region User directory
+
+    [HttpPost("client/v3/user_directory/search")]
+    public async Task<UserDirectoryResponse> SearchUserDirectory([FromBody] UserDirectoryRequest request) {
+        var users = roomStore._rooms
+            .SelectMany(x => x.State.Where(y =>
+                    y.Type == RoomMemberEventContent.EventId
+                    && y.RawContent?["membership"]?.ToString() == "join"
+                    && (y.StateKey!.ContainsAnyCase(request.SearchTerm) || y.RawContent?["displayname"]?.ToString()?.ContainsAnyCase(request.SearchTerm) == true)
+                )
+            )
+            .DistinctBy(x => x.StateKey)
+            .ToList();
+
+        request.Limit ??= 10;
+
+        return new() {
+            Results = users.Select(x => new UserDirectoryResponse.UserDirectoryResult {
+                UserId = x.StateKey!,
+                DisplayName = x.RawContent?["displayname"]?.ToString(),
+                AvatarUrl = x.RawContent?["avatar_url"]?.ToString()
+            }).ToList(),
+            Limited = users.Count > request.Limit
+        };
+    }
+
+#endregion
 }
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
index 7898a8c..18cccf6 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
@@ -11,6 +11,25 @@
 // [ApiController]
 // [Route("/_matrix/client/{version}/")]
 // public class KeysController(ILogger<KeysController> logger, TokenService tokenService, UserStore userStore) : ControllerBase {
+//     [HttpPost("keys/upload")]
+//     public async Task<object> UploadKeys(DeviceKeysUploadRequest request) {
+//         var token = tokenService.GetAccessToken(HttpContext);
+//         if (token == null)
+//             throw new MatrixException() {
+//                 ErrorCode = "M_MISSING_TOKEN",
+//                 Error = "Missing token"
+//             };
+//
+//         var user = await userStore.GetUserByToken(token);
+//         if (user == null)
+//             throw new MatrixException() {
+//                 ErrorCode = "M_UNKNOWN_TOKEN",
+//                 Error = "No such user"
+//             };
+//
+//         return new { };
+//     }
+//     
 //     [HttpGet("room_keys/version")]
 //     public async Task<RoomKeysResponse> GetRoomKeys() {
 //         var token = tokenService.GetAccessToken(HttpContext);
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
index 7899ada..59d37ff 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
@@ -42,6 +42,9 @@ public class MediaController(
 
     [HttpGet("download/{serverName}/{mediaId}")]
     public async Task DownloadMedia(string serverName, string mediaId) {
+        Response.Headers["Access-Control-Allow-Origin"] = "*";
+        Response.Headers["Access-Control-Allow-Methods"] = "GET";
+        
         var stream = await DownloadRemoteMedia(serverName, mediaId);
         await stream.CopyToAsync(Response.Body);
     }
@@ -56,6 +59,7 @@ public class MediaController(
         JsonObject data = new();
 
         using var hc = new HttpClient();
+        logger.LogInformation("Getting URL preview for {}", url);
         using var response = await hc.GetAsync(url);
         var doc = await response.Content.ReadAsStringAsync();
         var match = Regex.Match(doc, "<meta property=\"(.*?)\" content=\"(.*?)\"");
@@ -80,6 +84,7 @@ public class MediaController(
                     };
                 using var client = new HttpClient();
                 var stream = await client.GetStreamAsync(mediaUrl);
+                Directory.CreateDirectory(Path.GetDirectoryName(path)!);
                 await using var fs = System.IO.File.Create(path);
                 await stream.CopyToAsync(fs);
             }
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
index afd69d1..7a16ace 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
 using System.Diagnostics;
 using System.Text.Json.Nodes;
 using ArcaneLibs;
+using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes.Spec;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Helpers;
@@ -46,7 +47,7 @@ public class RoomTimelineController(
 
         room.Timeline.Add(evt);
         if (evt.Type == RoomMessageEventContent.EventId && (evt.TypedContent as RoomMessageEventContent).Body.StartsWith("!hse"))
-            await HandleHseCommand(evt, room, user);
+            _ = Task.Run(() => HandleHseCommand(evt, room, user));
         // else
 
         return new() {
@@ -124,9 +125,10 @@ public class RoomTimelineController(
 
         return evt;
     }
-    
+
     [HttpGet("relations/{eventId}")]
-    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
+    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null,
+        [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
         var token = tokenService.GetAccessToken(HttpContext);
         var user = await userStore.GetUserByToken(token);
 
@@ -156,9 +158,10 @@ public class RoomTimelineController(
             Chunk = matchingEvents.ToList()
         };
     }
-    
+
     [HttpGet("relations/{eventId}/{relationType}")]
-    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
+    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b",
+        [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
         var token = tokenService.GetAccessToken(HttpContext);
         var user = await userStore.GetUserByToken(token);
 
@@ -188,9 +191,10 @@ public class RoomTimelineController(
             Chunk = matchingEvents.ToList()
         };
     }
-    
+
     [HttpGet("relations/{eventId}/{relationType}/{eventType}")]
-    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
+    public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b",
+        [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) {
         var token = tokenService.GetAccessToken(HttpContext);
         var user = await userStore.GetUserByToken(token);
 
@@ -220,7 +224,7 @@ public class RoomTimelineController(
             Chunk = matchingEvents.ToList()
         };
     }
-    
+
     private async Task<IEnumerable<StateEventResponse>> GetRelationsInternal(string roomId, string eventId, string dir, string? from, int? limit, bool? recurse, string? to) {
         var room = roomStore.GetRoomById(roomId);
         var evt = room.Timeline.SingleOrDefault(x => x.EventId == eventId);
@@ -237,7 +241,7 @@ public class RoomTimelineController(
         else if (dir == "f") {
             relatedEvents = relatedEvents.Take(limit ?? 100);
         }
-        
+
         return relatedEvents;
     }
 
@@ -260,12 +264,14 @@ public class RoomTimelineController(
     }
 
     private async Task HandleHseCommand(StateEventResponse evt, RoomStore.Room room, UserStore.User user) {
+        logger.LogWarning("Handling HSE command for {0}: {1}", user.UserId, evt.RawContent.ToJson(false, true));
         try {
             var msgContent = evt.TypedContent as RoomMessageEventContent;
             var parts = msgContent.Body.Split('\n')[0].Split(" ");
             if (parts.Length < 2) return;
 
             var command = parts[1];
+            Console.WriteLine($"Handling command {command}");
             switch (command) {
                 case "import":
                     await HandleImportCommand(parts[2..], evt, room, user);
@@ -306,6 +312,7 @@ public class RoomTimelineController(
                             } while (Process.GetCurrentProcess().WorkingSet64 >= 1_024_000_000);
                         }
                     }
+
                     break;
                 }
                 case "genrooms": {
@@ -334,9 +341,11 @@ public class RoomTimelineController(
                                 }.ToStateEvent(user, room));
                             }
                         }
+
                         var newRoom = roomStore.CreateRoom(crq);
                         newRoom.AddUser(user.UserId);
                     }
+
                     InternalSendMessage(room, $"Generated {count} new rooms in {sw.Elapsed}!");
                     break;
                 }
@@ -348,6 +357,21 @@ public class RoomTimelineController(
                     InternalSendMessage(room,
                         $"GC memory: {Util.BytesToString(GC.GetTotalMemory(false))}, total process memory: {Util.BytesToString(Process.GetCurrentProcess().WorkingSet64)}");
                     break;
+                case "leave-all-rooms": {
+                    var rooms = roomStore.GetRoomsByMember(user.UserId);
+                    foreach (var memberEvt in rooms) {
+                        var roomObj = roomStore.GetRoomById(memberEvt.RoomId);
+                        roomObj.SetStateInternal(new() {
+                            Type = RoomMemberEventContent.EventId,
+                            StateKey = user.UserId,
+                            TypedContent = new RoomMemberEventContent() {
+                                Membership = "leave"
+                            },
+                        }, senderId: user.UserId);
+                    }
+
+                    break;
+                }
                 default:
                     InternalSendMessage(room, $"Command {command} not found!");
                     break;
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
index 024d071..f585eed 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
@@ -28,10 +28,9 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
         UserStore.User.SessionInfo.UserSyncState newSyncState = new();
 
         SyncResponse syncResp;
-        if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.ContainsKey(since))
+        if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.TryGetValue(since, out var syncState))
             syncResp = InitialSync(user, session);
         else {
-            var syncState = session.SyncStates[since];
             newSyncState = syncState.Clone();
 
             var newSyncToken = Guid.NewGuid().ToString();
@@ -155,7 +154,10 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
             }
         }
 
-        if (data.Join.Count > 0) return data;
+        if (data.Join.Count > 0) {
+            logger.LogTrace("Found {count} updated rooms", data.Join.Count);
+            return data;
+        }
 
         // step 2: check newly joined rooms
         var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList();
@@ -206,6 +208,10 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
 
 #endregion
 
+    private bool HasData(SyncResponse resp) {
+        return resp.Rooms?.Invite?.Count > 0 || resp.Rooms?.Join?.Count > 0 || resp.Rooms?.Leave?.Count > 0;
+    }
+    
     private async Task<bool> HasDataOrStall(SyncResponse resp) {
         // logger.LogTrace("Checking if sync response has data: {resp}", resp.ToJson(indent: false, ignoreNull: true));
         // if (resp.AccountData?.Events?.Count > 0) return true;
@@ -256,6 +262,14 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
             ToDevice: {
                 Events: { Count: > 0 }
             }
+        } or {
+            Rooms: {
+                Invite: { Count: > 0 }
+            } or {
+                Join: { Count: > 0 }
+            } or {
+                Leave: { Count: > 0 }
+            }
         };
 
         if (!hasData) {
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs
index 73b0d23..bcfb629 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs
@@ -27,6 +27,8 @@ public class HSEConfiguration {
     public string DataStoragePath { get; set; }
 
     public bool StoreData { get; set; } = true;
+
+    public string ServerName { get; set; } = "localhost";
     
     public bool UnknownSyncTokenIsInitialSync { get; set; } = true;
 
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs
index 4ce9f92..4684b01 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs
@@ -13,9 +13,11 @@ namespace LibMatrix.HomeserverEmulator.Services;
 
 public class UserStore {
     public ConcurrentBag<User> _users = new();
+    private readonly HSEConfiguration _config;
     private readonly RoomStore _roomStore;
 
     public UserStore(HSEConfiguration config, RoomStore roomStore) {
+        _config = config;
         _roomStore = roomStore;
         if (config.StoreData) {
             var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users");
@@ -64,12 +66,15 @@ public class UserStore {
         };
     }
 
-    public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null) {
+    public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null, string kind = "user") {
         profile ??= new();
-        if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]);
+        var parts = userId.Split(":");
+        var localPart = parts[0].TrimStart('@');
+        if (!profile.ContainsKey("displayname")) profile.Add("displayname", localPart);
         if (!profile.ContainsKey("avatar_url")) profile.Add("avatar_url", null);
         var user = new User() {
-            UserId = userId,
+            UserId = $"@{localPart}:{_config.ServerName}",
+            IsGuest = kind == "guest",
             AccountData = new() {
                 new StateEventResponse() {
                     Type = "im.vector.analytics",
@@ -80,7 +85,22 @@ public class UserStore {
                 new StateEventResponse() {
                     Type = "im.vector.web.settings",
                     RawContent = new JsonObject() {
-                        ["developerMode"] = true
+                        ["developerMode"] = true,
+                        ["alwaysShowTimestamps"] = true,
+                        ["SpotlightSearch.showNsfwPublicRooms"] = true,
+                        
+                    }
+                },
+                new() {
+                    Type = "im.vector.setting.integration_provisioning",
+                    RawContent = new JsonObject() {
+                        ["enabled"] = false
+                    }
+                },
+                new() {
+                    Type = "m.identity_server",
+                    RawContent = new JsonObject() {
+                        ["base_url"] = null
                     }
                 },
             }
@@ -185,6 +205,8 @@ public class UserStore {
             }
         }
 
+        public bool IsGuest { get; set; }
+
         public async Task SaveDebounced() {
             if (!HSEConfiguration.Current.StoreData) return;
             await _debounceCts.CancelAsync();
@@ -205,6 +227,7 @@ public class UserStore {
 
         public class SessionInfo {
             public string DeviceId { get; set; } = Guid.NewGuid().ToString();
+            public string DeviceName { get; set; } = "Unnamed device";
             public Dictionary<string, UserSyncState> SyncStates { get; set; } = new();
 
             public class UserSyncState {