about summary refs log tree commit diff
path: root/Tests/LibMatrix.HomeserverEmulator/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Tests/LibMatrix.HomeserverEmulator/Controllers')
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs40
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs2
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs103
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs56
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs34
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs58
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs106
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs130
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs119
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs81
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs58
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs52
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs)43
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs44
14 files changed, 885 insertions, 41 deletions
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
index d0496bf..da56ec4 100644
--- a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
@@ -8,16 +8,42 @@ namespace LibMatrix.HomeserverEmulator.Controllers;
 

 [ApiController]

 [Route("/_matrix/client/{version}/")]

-public class AuthController(ILogger<AuthController> logger, UserStore userStore) : ControllerBase {

+public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService) : ControllerBase {

     [HttpPost("login")]

     public async Task<LoginResponse> Login(LoginRequest request) {

-        var user = await userStore.CreateUser($"@{Guid.NewGuid().ToString()}:{Request.Host}", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary<string, object>());

-        var loginResponse = new LoginResponse {

-            AccessToken = user.AccessToken,

-            DeviceId = user.DeviceId,

-            UserId = user.UserId

+        if(!request.Identifier.User.StartsWith('@'))

+            request.Identifier.User = $"@{request.Identifier.User}:{tokenService.GenerateServerName(HttpContext)}";

+        if(request.Identifier.User.EndsWith("localhost"))

+            request.Identifier.User = request.Identifier.User.Replace("localhost", tokenService.GenerateServerName(HttpContext));

+        

+        var user = await userStore.GetUserById(request.Identifier.User);

+        if(user is null) {

+            user = await userStore.CreateUser(request.Identifier.User);

+        }

+

+        return user.Login();

+    }

+

+    [HttpGet("login")]

+    public async Task<LoginFlowsResponse> GetLoginFlows() {

+        return new LoginFlowsResponse {

+            Flows = ((string[]) [

+                "m.login.password",

+                "m.login.recaptcha",

+                "m.login.sso",

+                "m.login.email.identity",

+                "m.login.msisdn",

+                "m.login.dummy",

+                "m.login.registration_token",

+            ]).Select(x => new LoginFlowsResponse.LoginFlow { Type = x }).ToList()

         };

+    }

+}

+

+public class LoginFlowsResponse {

+    public required List<LoginFlow> Flows { get; set; }

 

-        return loginResponse;

+    public class LoginFlow {

+        public required string Type { get; set; }

     }

 }
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
index 0c4d8bd..9e0c17c 100644
--- a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
@@ -8,7 +8,7 @@ namespace LibMatrix.HomeserverEmulator.Controllers;
 public class HEDebugController(ILogger<HEDebugController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase {

     [HttpGet("users")]

     public async Task<List<UserStore.User>> GetUsers() {

-        return userStore._users;

+        return userStore._users.ToList();

     }

     

     [HttpGet("rooms")]

diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
new file mode 100644
index 0000000..7898a8c
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
@@ -0,0 +1,103 @@
+// using System.Security.Cryptography;
+// using System.Text.Json.Nodes;
+// using System.Text.Json.Serialization;
+// using LibMatrix.HomeserverEmulator.Services;
+// using LibMatrix.Responses;
+// using LibMatrix.Services;
+// using Microsoft.AspNetCore.Mvc;
+//
+// namespace LibMatrix.HomeserverEmulator.Controllers;
+//
+// [ApiController]
+// [Route("/_matrix/client/{version}/")]
+// public class KeysController(ILogger<KeysController> logger, TokenService tokenService, UserStore userStore) : ControllerBase {
+//     [HttpGet("room_keys/version")]
+//     public async Task<RoomKeysResponse> GetRoomKeys() {
+//         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"
+//             };
+//
+//         if (user.RoomKeys is not { Count: > 0 })
+//             throw new MatrixException() {
+//                 ErrorCode = "M_NOT_FOUND",
+//                 Error = "No keys found"
+//             };
+//
+//         return user.RoomKeys.Values.Last();
+//     }
+//
+//     [HttpPost("room_keys/version")]
+//     public async Task<RoomKeysResponse> UploadRoomKeys(RoomKeysRequest 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"
+//             };
+//
+//         var roomKeys = new RoomKeysResponse {
+//             Version = Guid.NewGuid().ToString(),
+//             Etag = Guid.NewGuid().ToString(),
+//             Algorithm = request.Algorithm,
+//             AuthData = request.AuthData
+//         };
+//         user.RoomKeys.Add(roomKeys.Version, roomKeys);
+//         return roomKeys;
+//     }
+//     
+//     [HttpPost("keys/device_signing/upload")]
+//     public async Task<object> UploadDeviceSigning(JsonObject 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 { };
+//     }
+// }
+//
+// public class DeviceSigningRequest {
+//     public CrossSigningKey? MasterKey { get; set; }
+//     public CrossSigningKey? SelfSigningKey { get; set; }
+//     public CrossSigningKey? UserSigningKey { get; set; }
+//     
+//     public class CrossSigningKey {
+//         [JsonPropertyName("keys")]
+//         public Dictionary<string, string> Keys { get; set; }
+//         
+//         [JsonPropertyName("signatures")]
+//         public Dictionary<string, Dictionary<string, string>> Signatures { get; set; }
+//         
+//         [JsonPropertyName("usage")]
+//         public List<string> Usage { get; set; }
+//         
+//         [JsonPropertyName("user_id")]
+//         public string UserId { get; set; }
+//     }
+// }
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs
new file mode 100644
index 0000000..e3f781b
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Cryptography;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers;
+
+[ApiController]
+[Route("/_matrix/client/{version}/")]
+public class LegacyController(ILogger<LegacyController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {
+    [HttpGet("rooms/{roomId}/initialSync")]
+    [SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", Justification = "Endpoint is expected to wait until data is available or timeout.")]
+    public async Task<object> Sync([FromRoute] string roomId, [FromQuery] int limit = 20) {
+        var sw = Stopwatch.StartNew();
+        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"
+            };
+        var room = roomStore.GetRoomById(roomId);
+        if (room is null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found."
+            };
+        var accountData = room.AccountData.GetOrCreate(user.UserId, _ => []);
+        var membership = room.State.FirstOrDefault(x => x.Type == "m.room.member" && x.StateKey == user.UserId);
+        var timelineChunk = room.Timeline.TakeLast(limit).ToList();
+        return new {
+            account_data = accountData,
+            membership = (membership?.TypedContent as RoomMemberEventContent)?.Membership ?? "leave",
+            room_id = room.RoomId,
+            state = room.State.ToList(),
+            visibility = "public",
+            messages = new PaginatedChunkedStateEventResponse() {
+                Chunk = timelineChunk,
+                End = timelineChunk.Last().EventId,
+                Start = timelineChunk.Count >= limit ? timelineChunk.First().EventId : null
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
new file mode 100644
index 0000000..dba36d7
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
@@ -0,0 +1,34 @@
+using LibMatrix.HomeserverEmulator.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers.Media;
+
+[ApiController]
+[Route("/_matrix/media/{version}/")]
+public class MediaController(ILogger<MediaController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {
+    [HttpPost("upload")]
+    public async Task<object> UploadMedia([FromHeader(Name = "Content-Type")] string ContentType, [FromQuery] string filename, [FromBody] Stream file) {
+        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"
+            };
+        
+        
+        
+        var mediaId = Guid.NewGuid().ToString();
+        var media = new {
+            content_uri = $"mxc://{tokenService.GenerateServerName(HttpContext)}/{mediaId}"
+        };
+        return media;
+        
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs
new file mode 100644
index 0000000..d5f4217
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs
@@ -0,0 +1,58 @@
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.OpenApi.Validations.Rules;
+
+namespace LibMatrix.HomeserverEmulator.Controllers.Rooms;
+
+[ApiController]
+[Route("/_matrix/client/{version}/rooms/{roomId}/")]
+public class RoomMembersController(ILogger<RoomMembersController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {
+    [HttpGet("members")]
+    public async Task<List<StateEventResponse>> CreateRoom(string roomId, string? at = null, string? membership = null, string? not_membership = null) {
+        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"
+            };
+        
+        var room = roomStore.GetRoomById(roomId);
+        if (room == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found"
+            };
+        
+        var members = room.State.Where(x => x.Type == "m.room.member").ToList();
+        
+        if(membership != null)
+            members = members.Where(x => (x.TypedContent as RoomMemberEventContent)?.Membership == membership).ToList();
+        
+        if(not_membership != null)
+            members = members.Where(x => (x.TypedContent as RoomMemberEventContent)?.Membership != not_membership).ToList();
+
+        if (at != null) {
+            var evt = room.Timeline.FirstOrDefault(x => x.EventId == at);
+            if (evt == null)
+                throw new MatrixException() {
+                    ErrorCode = "M_NOT_FOUND",
+                    Error = "Event not found"
+                };
+            
+            members = members.Where(x => x.OriginServerTs <= evt.OriginServerTs).ToList();
+        }
+
+        return members;
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
new file mode 100644
index 0000000..593f5b0
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
@@ -0,0 +1,106 @@
+using System.Collections.Frozen;
+using LibMatrix.HomeserverEmulator.Extensions;
+using LibMatrix.HomeserverEmulator.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers.Rooms;
+
+[ApiController]
+[Route("/_matrix/client/{version}/rooms/{roomId}/state")]
+public class RoomStateController(ILogger<RoomStateController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {
+    [HttpGet("")]
+    public async Task<FrozenSet<StateEventResponse>> GetState(string roomId) {
+        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"
+            };
+        
+        var room = roomStore.GetRoomById(roomId);
+        if (room == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found"
+            };
+
+        return room.State;
+    }
+    
+    [HttpGet("{eventType}")]
+    public async Task<StateEventResponse> GetState(string roomId, string eventType) {
+        return await GetState(roomId, eventType, "");
+    }
+    
+    [HttpGet("{eventType}/{stateKey}")]
+    public async Task<StateEventResponse> GetState(string roomId, string eventType, string stateKey) {
+        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"
+            };
+        
+        var room = roomStore.GetRoomById(roomId);
+        if (room == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found"
+            };
+
+        var stateEvent = room.State.FirstOrDefault(x => x.Type == eventType && x.StateKey == stateKey);
+        if (stateEvent == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Event not found"
+            };
+        return stateEvent;
+    }
+        
+    [HttpPut("{eventType}")]
+    public async Task<EventIdResponse> SetState(string roomId, string eventType, [FromBody] StateEvent request) {
+        return await SetState(roomId, eventType, "", request);
+    }
+    
+    [HttpPut("{eventType}/{stateKey}")]
+    public async Task<EventIdResponse> SetState(string roomId, string eventType, string stateKey, [FromBody] StateEvent 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"
+            };
+        
+        var room = roomStore.GetRoomById(roomId);
+        if (room == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found"
+            };
+        var evt = room.SetStateInternal(request.ToStateEvent(user, room));
+        evt.Type = eventType;
+        evt.StateKey = stateKey;
+        return new EventIdResponse(evt);
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs
new file mode 100644
index 0000000..e9f52dc
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs
@@ -0,0 +1,130 @@
+using System.Text.Json.Serialization;
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers.Rooms;
+
+[ApiController]
+[Route("/_matrix/client/{version}/")]
+public class RoomsController(ILogger<RoomsController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {
+    //createRoom
+    [HttpPost("createRoom")]
+    public async Task<RoomIdResponse> CreateRoom([FromBody] CreateRoomRequest 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"
+            };
+
+        var room = new RoomStore.Room($"!{Guid.NewGuid()}:{tokenService.GenerateServerName(HttpContext)}");
+        var createEvent = room.SetStateInternal(new() {
+            Type = RoomCreateEventContent.EventId,
+            RawContent = new() {
+                ["creator"] = user.UserId
+            }
+        });
+        foreach (var (key, value) in request.CreationContent) {
+            createEvent.RawContent[key] = value.DeepClone();
+        }
+
+        if (!string.IsNullOrWhiteSpace(request.Name))
+            room.SetStateInternal(new StateEvent() {
+                Type = RoomNameEventContent.EventId,
+                TypedContent = new RoomNameEventContent() {
+                    Name = request.Name
+                }
+            });
+
+        if (!string.IsNullOrWhiteSpace(request.RoomAliasName))
+            room.SetStateInternal(new StateEvent() {
+                Type = RoomCanonicalAliasEventContent.EventId,
+                TypedContent = new RoomCanonicalAliasEventContent() {
+                    Alias = $"#{request.RoomAliasName}:localhost"
+                }
+            });
+
+        if (request.InitialState is { Count: > 0 }) {
+            foreach (var stateEvent in request.InitialState) {
+                room.SetStateInternal(stateEvent);
+            }
+        }
+
+        room.AddUser(user.UserId);
+
+        // user.Rooms.Add(room.RoomId, room);
+        return new() {
+            RoomId = room.RoomId
+        };
+    }
+
+    [HttpPost("rooms/{roomId}/upgrade")]
+    public async Task<object> UpgradeRoom(string roomId, [FromBody] UpgradeRoomRequest 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"
+            };
+
+        var oldRoom = roomStore.GetRoomById(roomId);
+        if (oldRoom == null)
+            throw new MatrixException() {
+                ErrorCode = "M_NOT_FOUND",
+                Error = "Room not found"
+            };
+
+        var room = new RoomStore.Room($"!{Guid.NewGuid()}:{tokenService.GenerateServerName(HttpContext)}");
+
+        var eventTypesToTransfer = new[] {
+            RoomServerACLEventContent.EventId,
+            RoomEncryptionEventContent.EventId,
+            RoomNameEventContent.EventId,
+            RoomAvatarEventContent.EventId,
+            RoomTopicEventContent.EventId,
+            RoomGuestAccessEventContent.EventId,
+            RoomHistoryVisibilityEventContent.EventId,
+            RoomJoinRulesEventContent.EventId,
+            RoomPowerLevelEventContent.EventId,
+        };
+
+        var createEvent = room.SetStateInternal(new() {
+            Type = RoomCreateEventContent.EventId,
+            RawContent = new() {
+                ["creator"] = user.UserId
+            }
+        });
+        
+        oldRoom.State.Where(x => eventTypesToTransfer.Contains(x.Type)).ToList().ForEach(x => room.SetStateInternal(x));
+
+        room.AddUser(user.UserId);
+
+        // user.Rooms.Add(room.RoomId, room);
+        return new {
+            replacement_room = room.RoomId
+        };
+    }
+}
+
+public class UpgradeRoomRequest {
+    [JsonPropertyName("new_version")]
+    public required string NewVersion { get; set; }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
new file mode 100644
index 0000000..1653110
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
@@ -0,0 +1,119 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Cryptography;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
+using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers;
+
+[ApiController]
+[Route("/_matrix/client/{version}/")]
+public class SyncController(ILogger<SyncController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore, HSEConfiguration cfg) : ControllerBase {
+    [HttpGet("sync")]
+    [SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", Justification = "Endpoint is expected to wait until data is available or timeout.")]
+    public async Task<SyncResponse> Sync([FromQuery] string? since = null, [FromQuery] int? timeout = 5000) {
+        var sw = Stopwatch.StartNew();
+        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"
+            };
+        var session = user.AccessTokens[token];
+
+        if (string.IsNullOrWhiteSpace(since))
+            return InitialSync(user, session);
+
+        if (!session.SyncStates.TryGetValue(since, out var syncState))
+            if (!cfg.UnknownSyncTokenIsInitialSync)
+                throw new MatrixException() {
+                    ErrorCode = "M_UNKNOWN",
+                    Error = "Unknown sync token."
+                };
+            else
+                return InitialSync(user, session);
+
+        var response = new SyncResponse() {
+            NextBatch = Guid.NewGuid().ToString(),
+            DeviceOneTimeKeysCount = new()
+        };
+
+        session.SyncStates.Add(response.NextBatch, new() {
+            RoomPositions = syncState.RoomPositions.ToDictionary(x => x.Key, x => new UserStore.User.SessionInfo.UserSyncState.SyncRoomPosition() {
+                StatePosition = roomStore._rooms.First(y => y.RoomId == x.Key).State.Count,
+                TimelinePosition = roomStore._rooms.First(y => y.RoomId == x.Key).Timeline.Count,
+                AccountDataPosition = roomStore._rooms.First(y => y.RoomId == x.Key).AccountData[user.UserId].Count
+            })
+        });
+
+        if (!string.IsNullOrWhiteSpace(since)) {
+            while (sw.ElapsedMilliseconds < timeout && response.Rooms?.Join is not { Count: > 0 }) {
+                await Task.Delay(100);
+                var rooms = roomStore._rooms.Where(x => x.State.Any(y => y.Type == "m.room.member" && y.StateKey == user.UserId)).ToList();
+                foreach (var room in rooms) {
+                    var roomPositions = syncState.RoomPositions[room.RoomId];
+
+                    response.Rooms ??= new();
+                    response.Rooms.Join ??= new();
+                    response.Rooms.Join[room.RoomId] = new() {
+                        State = new(room.State.Skip(roomPositions.StatePosition).ToList()),
+                        Timeline = new(events: room.Timeline.Skip(roomPositions.TimelinePosition).ToList(), limited: false),
+                        AccountData = new(room.AccountData.GetOrCreate(user.UserId, _ => []).Skip(roomPositions.AccountDataPosition).ToList())
+                    };
+                    session.SyncStates[response.NextBatch].RoomPositions[room.RoomId] = new() {
+                        StatePosition = room.State.Count,
+                        TimelinePosition = room.Timeline.Count,
+                        AccountDataPosition = room.AccountData[user.UserId].Count
+                    };
+
+                    if (response.Rooms.Join[room.RoomId].State.Events.Count == 0 &&
+                        response.Rooms.Join[room.RoomId].Timeline.Events.Count == 0 &&
+                        response.Rooms.Join[room.RoomId].AccountData.Events.Count == 0
+                       )
+                        response.Rooms.Join.Remove(room.RoomId);
+                }
+            }
+        }
+
+        return response;
+    }
+
+    private SyncResponse InitialSync(UserStore.User user, UserStore.User.SessionInfo session) {
+        var response = new SyncResponse() {
+            NextBatch = Guid.NewGuid().ToString(),
+            DeviceOneTimeKeysCount = new(),
+            AccountData = new(events: user.AccountData.ToList())
+        };
+
+        session.SyncStates.Add(response.NextBatch, new());
+
+        var rooms = roomStore._rooms.Where(x => x.State.Any(y => y.Type == "m.room.member" && y.StateKey == user.UserId)).ToList();
+        foreach (var room in rooms) {
+            response.Rooms ??= new();
+            response.Rooms.Join ??= new();
+            response.Rooms.Join[room.RoomId] = new() {
+                State = new(room.State.ToList()),
+                Timeline = new(events: room.Timeline.ToList(), limited: false),
+                AccountData = new(room.AccountData.GetOrCreate(user.UserId, _ => []).ToList())
+            };
+            session.SyncStates[response.NextBatch].RoomPositions[room.RoomId] = new() {
+                StatePosition = room.State.Count,
+                TimelinePosition = room.Timeline.Count,
+                AccountDataPosition = room.AccountData[user.UserId].Count
+            };
+        }
+
+        return response;
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs
new file mode 100644
index 0000000..8cd5c75
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs
@@ -0,0 +1,81 @@
+using System.Text.Json.Nodes;

+using ArcaneLibs.Extensions;

+using LibMatrix.EventTypes.Spec.State;

+using LibMatrix.Filters;

+using LibMatrix.HomeserverEmulator.Services;

+using LibMatrix.Responses;

+using Microsoft.AspNetCore.Mvc;

+

+namespace LibMatrix.HomeserverEmulator.Controllers;

+

+[ApiController]

+[Route("/_matrix/client/{version}/")]

+public class AccountDataController(ILogger<AccountDataController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {

+    [HttpGet("user/{mxid}/account_data/{type}")]

+    public async Task<object> GetAccountData(string type) {

+        var token = tokenService.GetAccessToken(HttpContext);

+        if (token is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "No token passed."

+            };

+

+        var user = await userStore.GetUserByToken(token, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "Invalid token."

+            };

+        var value = user.AccountData.FirstOrDefault(x=>x.Type == type);

+        if (value is null)

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "Key not found."

+            };

+        return value;

+    }

+    

+    [HttpPut("user/{mxid}/account_data/{type}")]

+    public async Task<object> SetAccountData(string type, [FromBody] JsonObject data) {

+        var token = tokenService.GetAccessToken(HttpContext);

+        if (token is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "No token passed."

+            };

+

+        var user = await userStore.GetUserByToken(token, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "Invalid token."

+            };

+        

+        user.AccountData.Where(x=>x.Type == type).ToList().ForEach(response => user.AccountData.Remove(response));

+        

+        user.AccountData.Add(new() {

+            Type = type,

+            RawContent = data

+        });

+        return data;

+    }

+    

+    // specialised account data...

+    [HttpGet("pushrules")]

+    public async Task<object> GetPushRules() {

+        var token = tokenService.GetAccessToken(HttpContext);

+        if (token is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "No token passed."

+            };

+

+        var user = await userStore.GetUserByToken(token, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "Invalid token."

+            };

+        return new { };

+    }

+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs
new file mode 100644
index 0000000..ecbccd4
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Nodes;

+using ArcaneLibs.Extensions;

+using LibMatrix.EventTypes.Spec.State;

+using LibMatrix.Filters;

+using LibMatrix.HomeserverEmulator.Services;

+using LibMatrix.Responses;

+using Microsoft.AspNetCore.Mvc;

+

+namespace LibMatrix.HomeserverEmulator.Controllers;

+

+[ApiController]

+[Route("/_matrix/client/{version}/")]

+public class FilterController(ILogger<FilterController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {

+    [HttpPost("user/{mxid}/filter")]

+    public async Task<object> CreateFilter(string mxid, [FromBody] SyncFilter filter) {

+        var token = tokenService.GetAccessToken(HttpContext);

+        if (token is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "No token passed."

+            };

+

+        var user = await userStore.GetUserByToken(token, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "Invalid token."

+            };

+        var filterId = Guid.NewGuid().ToString();

+        user.Filters[filterId] = filter;

+        return new {

+            filter_id = filterId

+        };

+    }

+    

+    [HttpGet("user/{mxid}/filter/{filterId}")]

+    public async Task<SyncFilter> GetFilter(string mxid, string filterId) {

+        var token = tokenService.GetAccessToken(HttpContext);

+        if (token is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "No token passed."

+            };

+

+        var user = await userStore.GetUserByToken(token, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_UNAUTHORIZED",

+                Error = "Invalid token."

+            };

+        if (!user.Filters.ContainsKey(filterId))

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "Filter not found."

+            };

+        return user.Filters[filterId];

+    }

+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs
new file mode 100644
index 0000000..c717ba5
--- /dev/null
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Nodes;

+using ArcaneLibs.Extensions;

+using LibMatrix.EventTypes.Spec.State;

+using LibMatrix.Filters;

+using LibMatrix.HomeserverEmulator.Services;

+using LibMatrix.Responses;

+using Microsoft.AspNetCore.Mvc;

+

+namespace LibMatrix.HomeserverEmulator.Controllers;

+

+[ApiController]

+[Route("/_matrix/client/{version}/")]

+public class ProfileController(ILogger<ProfileController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {

+    [HttpGet("profile/{userId}")]

+    public async Task<IDictionary<string, object>> GetProfile(string userId) {

+        var user = await userStore.GetUserById(userId, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "User not found."

+            };

+        return user.Profile;

+    }

+

+    [HttpGet("profile/{userId}/{key}")]

+    public async Task<object> GetProfile(string userId, string key) {

+        var user = await userStore.GetUserById(userId, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "User not found."

+            };

+        if (!user.Profile.TryGetValue(key, out var value))

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "Key not found."

+            };

+        return value;

+    }

+

+    [HttpPut("profile/{userId}/{key}")]

+    public async Task<object> SetProfile(string userId, string key, [FromBody] JsonNode value) {

+        var user = await userStore.GetUserById(userId, false);

+        if (user is null)

+            throw new MatrixException() {

+                ErrorCode = "M_NOT_FOUND",

+                Error = "User not found."

+            };

+        user.Profile[key] = value[key];

+        return value;

+    }

+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
index d763b26..eb2b879 100644
--- a/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
@@ -1,5 +1,7 @@
 using System.Text.Json.Nodes;

 using ArcaneLibs.Extensions;

+using LibMatrix.EventTypes.Spec.State;

+using LibMatrix.Filters;

 using LibMatrix.HomeserverEmulator.Services;

 using LibMatrix.Responses;

 using Microsoft.AspNetCore.Mvc;

@@ -8,17 +10,17 @@ namespace LibMatrix.HomeserverEmulator.Controllers;
 

 [ApiController]

 [Route("/_matrix/client/{version}/")]

-public class UserController(ILogger<UserController> logger, TokenService tokenService, UserStore userStore) : ControllerBase {

+public class UserController(ILogger<UserController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase {

     [HttpGet("account/whoami")]

     public async Task<WhoAmIResponse> Login() {

-        var token = tokenService.GetAccessToken();

+        var token = tokenService.GetAccessToken(HttpContext);

         if (token is null)

             throw new MatrixException() {

                 ErrorCode = "M_UNAUTHORIZED",

                 Error = "No token passed."

             };

 

-        var user = await userStore.GetUserByToken(token, Random.Shared.Next(101) <= 10, tokenService.GenerateServerName());

+        var user = await userStore.GetUserByToken(token, Random.Shared.Next(101) <= 10, tokenService.GenerateServerName(HttpContext));

         if (user is null)

             throw new MatrixException() {

                 ErrorCode = "M_UNKNOWN_TOKEN",

@@ -29,37 +31,10 @@ public class UserController(ILogger<UserController> logger, TokenService tokenSe
         };

         return whoAmIResponse;

     }

-    

-    [HttpGet("profile/{userId}")]

-    public async Task<Dictionary<string, object>> GetProfile(string userId) {

-        var user = await userStore.GetUserById(userId, false);

-        if (user is null)

-            throw new MatrixException() {

-                ErrorCode = "M_NOT_FOUND",

-                Error = "User not found."

-            };

-        return user.Profile;

-    }

-    

-    [HttpGet("profile/{userId}/{key}")]

-    public async Task<object> GetProfile(string userId, string key) {

-        var user = await userStore.GetUserById(userId, false);

-        if (user is null)

-            throw new MatrixException() {

-                ErrorCode = "M_NOT_FOUND",

-                Error = "User not found."

-            };

-        if (!user.Profile.TryGetValue(key, out var value))

-            throw new MatrixException() {

-                ErrorCode = "M_NOT_FOUND",

-                Error = "Key not found."

-            };

-        return value;

-    }

-    

+

     [HttpGet("joined_rooms")]

     public async Task<object> GetJoinedRooms() {

-        var token = tokenService.GetAccessToken();

+        var token = tokenService.GetAccessToken(HttpContext);

         if (token is null)

             throw new MatrixException() {

                 ErrorCode = "M_UNAUTHORIZED",

@@ -75,7 +50,9 @@ public class UserController(ILogger<UserController> logger, TokenService tokenSe
         // return user.JoinedRooms;

 

         return new {

-            joined_rooms = user.JoinedRooms

+            joined_rooms = roomStore._rooms.Where(r =>

+                r.State.Any(s => s.StateKey == user.UserId && s.Type == RoomMemberEventContent.EventId && (s.TypedContent as RoomMemberEventContent).Membership == "join")

+            ).Select(r => r.RoomId).ToList()

         };

     }

 }
\ No newline at end of file
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
index 1349fac..704e26b 100644
--- a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
+++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
@@ -1,4 +1,5 @@
 using System.Text.Json.Nodes;

+using System.Text.Json.Serialization;

 using LibMatrix.Homeservers;

 using LibMatrix.Responses;

 using LibMatrix.Services;

@@ -45,4 +46,47 @@ public class VersionsController(ILogger<WellKnownController> logger) : Controlle
         };

         return clientVersions;

     }

+

+    [HttpGet("client/{version}/capabilities")]

+    public async Task<CapabilitiesResponse> GetCapabilities() {

+        return new() {

+            Capabilities = new() {

+                ChangePassword = new() {

+                    Enabled = false

+                },

+                RoomVersions = new() {

+                    Default = "11",

+                    Available = new() {

+                        ["11"] = "unstable"

+                    }

+                }

+            }

+        };

+    }

+}

+

+public class CapabilitiesResponse {

+    [JsonPropertyName("capabilities")]

+    public CapabilitiesContent Capabilities { get; set; }

+

+    public class CapabilitiesContent {

+        [JsonPropertyName("m.room_versions")]

+        public RoomVersionsContent RoomVersions { get; set; }

+        

+        [JsonPropertyName("m.change_password")]

+        public ChangePasswordContent ChangePassword { get; set; }

+

+        public class ChangePasswordContent {

+            [JsonPropertyName("enabled")]

+            public bool Enabled { get; set; }

+        }

+

+        public class RoomVersionsContent {

+            [JsonPropertyName("default")]

+            public string Default { get; set; }

+

+            [JsonPropertyName("available")]

+            public Dictionary<string, string> Available { get; set; }

+        }

+    }

 }
\ No newline at end of file