From f41b6e5ec431c88bc1d94e4832d8ba49ddc42004 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Tue, 5 Mar 2024 11:19:52 +0100 Subject: HomeserverEmulator work --- .../Controllers/AuthController.cs | 40 +++++-- .../Controllers/HEDebug/HEDebugController.cs | 2 +- .../Controllers/KeysController.cs | 103 ++++++++++++++++ .../Controllers/LegacyController.cs | 56 +++++++++ .../Controllers/Media/MediaController.cs | 34 ++++++ .../Controllers/Rooms/RoomMembersController.cs | 58 +++++++++ .../Controllers/Rooms/RoomStateController.cs | 106 +++++++++++++++++ .../Controllers/Rooms/RoomsController.cs | 130 +++++++++++++++++++++ .../Controllers/SyncController.cs | 119 +++++++++++++++++++ .../Controllers/UserController.cs | 81 ------------- .../Controllers/Users/AccountDataController.cs | 81 +++++++++++++ .../Controllers/Users/FilterController.cs | 58 +++++++++ .../Controllers/Users/ProfileController.cs | 52 +++++++++ .../Controllers/Users/UserController.cs | 58 +++++++++ .../Controllers/VersionsController.cs | 44 +++++++ 15 files changed, 933 insertions(+), 89 deletions(-) create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs delete mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs create mode 100644 Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs (limited to 'Tests/LibMatrix.HomeserverEmulator/Controllers') 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 logger, UserStore userStore) : ControllerBase { +public class AuthController(ILogger logger, UserStore userStore, TokenService tokenService) : ControllerBase { [HttpPost("login")] public async Task Login(LoginRequest request) { - var user = await userStore.CreateUser($"@{Guid.NewGuid().ToString()}:{Request.Host}", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary()); - 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 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 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 logger, UserStore userStore, RoomStore roomStore) : ControllerBase { [HttpGet("users")] public async Task> 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 logger, TokenService tokenService, UserStore userStore) : ControllerBase { +// [HttpGet("room_keys/version")] +// public async Task 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 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 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 Keys { get; set; } +// +// [JsonPropertyName("signatures")] +// public Dictionary> Signatures { get; set; } +// +// [JsonPropertyName("usage")] +// public List 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 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 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpPost("upload")] + public async Task 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("members")] + public async Task> 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("")] + public async Task> 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 GetState(string roomId, string eventType) { + return await GetState(roomId, eventType, ""); + } + + [HttpGet("{eventType}/{stateKey}")] + public async Task 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 SetState(string roomId, string eventType, [FromBody] StateEvent request) { + return await SetState(roomId, eventType, "", request); + } + + [HttpPut("{eventType}/{stateKey}")] + public async Task 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + //createRoom + [HttpPost("createRoom")] + public async Task 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 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 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 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/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs deleted file mode 100644 index d763b26..0000000 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Text.Json.Nodes; -using ArcaneLibs.Extensions; -using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; -using Microsoft.AspNetCore.Mvc; - -namespace LibMatrix.HomeserverEmulator.Controllers; - -[ApiController] -[Route("/_matrix/client/{version}/")] -public class UserController(ILogger logger, TokenService tokenService, UserStore userStore) : ControllerBase { - [HttpGet("account/whoami")] - public async Task Login() { - var token = tokenService.GetAccessToken(); - 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()); - if (user is null) - throw new MatrixException() { - ErrorCode = "M_UNKNOWN_TOKEN", - Error = "Invalid token." - }; - var whoAmIResponse = new WhoAmIResponse { - UserId = user.UserId - }; - return whoAmIResponse; - } - - [HttpGet("profile/{userId}")] - public async Task> 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 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 GetJoinedRooms() { - var token = tokenService.GetAccessToken(); - 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 user.JoinedRooms; - - return new { - joined_rooms = user.JoinedRooms - }; - } -} \ 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("user/{mxid}/account_data/{type}")] + public async Task 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 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 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpPost("user/{mxid}/filter")] + public async Task 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 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 logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("profile/{userId}")] + public async Task> 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 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 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/Users/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs new file mode 100644 index 0000000..eb2b879 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.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 UserController(ILogger logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("account/whoami")] + public async Task Login() { + 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(HttpContext)); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "Invalid token." + }; + var whoAmIResponse = new WhoAmIResponse { + UserId = user.UserId + }; + return whoAmIResponse; + } + + [HttpGet("joined_rooms")] + public async Task GetJoinedRooms() { + 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 user.JoinedRooms; + + return new { + 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 logger) : Controlle }; return clientVersions; } + + [HttpGet("client/{version}/capabilities")] + public async Task 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 Available { get; set; } + } + } } \ No newline at end of file -- cgit 1.4.1