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
|