diff options
9 files changed, 175 insertions, 29 deletions
diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/EventIdResponse.cs index 0a7cfd9..4d715a4 100644 --- a/LibMatrix/EventIdResponse.cs +++ b/LibMatrix/EventIdResponse.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; namespace LibMatrix; -public class EventIdResponse(string eventId) { +public class EventIdResponse { [JsonPropertyName("event_id")] - public string EventId { get; set; } = eventId; + public string EventId { get; set; } } \ No newline at end of file diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index d9d0f4e..9800c27 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -72,7 +72,7 @@ public class StateEvent { } [JsonPropertyName("state_key")] - public string StateKey { get; set; } + public string? StateKey { get; set; } [JsonPropertyName("type")] public string Type { get; set; } diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs index 593f5b0..46c3062 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs @@ -101,6 +101,8 @@ public class RoomStateController(ILogger<RoomStateController> logger, TokenServi var evt = room.SetStateInternal(request.ToStateEvent(user, room)); evt.Type = eventType; evt.StateKey = stateKey; - return new EventIdResponse(evt); + return new EventIdResponse(){ + EventId = evt.EventId + }; } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs new file mode 100644 index 0000000..c9bdb57 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs @@ -0,0 +1,52 @@ +using System.Collections.Frozen; +using System.Text.Json.Nodes; +using LibMatrix.HomeserverEmulator.Extensions; +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers.Rooms; + +[ApiController] +[Route("/_matrix/client/{version}/rooms/{roomId}")] +public class RoomTimelineController(ILogger<RoomTimelineController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpPut("send/{eventType}/{txnId}")] + public async Task<EventIdResponse> SendMessage(string roomId, string eventType, string txnId, [FromBody] JsonObject content) { + 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" + }; + + if (!room.JoinedMembers.Any(x=>x.StateKey == user.UserId)) + throw new MatrixException() { + ErrorCode = "M_FORBIDDEN", + Error = "User is not in the room" + }; + + var evt = new StateEvent() { + RawContent = content, + Type = eventType + }.ToStateEvent(user, room); + + room.Timeline.Add(evt); + + return new() { + EventId = evt.EventId + }; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs index e9f52dc..b0f5014 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs @@ -112,7 +112,7 @@ public class RoomsController(ILogger<RoomsController> logger, TokenService token ["creator"] = user.UserId } }); - + oldRoom.State.Where(x => eventTypesToTransfer.Contains(x.Type)).ToList().ForEach(x => room.SetStateInternal(x)); room.AddUser(user.UserId); @@ -122,6 +122,38 @@ public class RoomsController(ILogger<RoomsController> logger, TokenService token replacement_room = room.RoomId }; } + + [HttpPost("rooms/{roomId}/leave")] // TODO: implement + public async Task<object> LeaveRoom(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" + }; + + // room.RemoveUser(user.UserId); + + // room.SetStateInternal(new StateEventResponse() { }); + + return new { + room_id = room.RoomId + }; + } } public class UpgradeRoomRequest { diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs index 1653110..afcf711 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs @@ -1,11 +1,8 @@ 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; @@ -51,7 +48,6 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe 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 }) @@ -67,19 +63,19 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe 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()) }; + if (response.Rooms.Join[room.RoomId].Timeline?.Events?.Count > 0) + response.Rooms.Join[room.RoomId].State = new(response.Rooms.Join[room.RoomId].Timeline!.Events.Where(x => x.StateKey != null).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 + 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); } @@ -108,7 +104,6 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe 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 }; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs index eb2b879..e886d46 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Filters; @@ -55,4 +56,43 @@ public class UserController(ILogger<UserController> logger, TokenService tokenSe ).Select(r => r.RoomId).ToList() }; } + + [HttpGet("devices")] + public async Task<DevicesResponse> GetDevices() { + 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() { + Devices = user.AccessTokens.Select(x=>new DevicesResponse.Device() { + DeviceId = x.Value.DeviceId, + DisplayName = x.Value.DeviceId + }).ToList() + }; + } + + public class DevicesResponse { + [JsonPropertyName("devices")] + public List<Device> Devices { get; set; } + + public class Device { + [JsonPropertyName("device_id")] + public string DeviceId { get; set; } + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } + [JsonPropertyName("last_seen_ip")] + public string LastSeenIp { get; set; } + [JsonPropertyName("last_seen_ts")] + public long LastSeenTs { get; set; } + } + } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs index 37d9c7d..d2c9e15 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Collections.Frozen; +using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; @@ -17,9 +19,9 @@ namespace LibMatrix.HomeserverEmulator.Services; public class RoomStore { public ConcurrentBag<Room> _rooms = new(); private Dictionary<string, Room> _roomsById = new(); - + public RoomStore(HSEConfiguration config) { - if(config.StoreData) { + if (config.StoreData) { var path = Path.Combine(config.DataStoragePath, "rooms"); if (!Directory.Exists(path)) Directory.CreateDirectory(path); foreach (var file in Directory.GetFiles(path)) { @@ -29,7 +31,7 @@ public class RoomStore { } else Console.WriteLine("Data storage is disabled, not loading rooms from disk"); - + RebuildIndexes(); } @@ -78,7 +80,7 @@ public class RoomStore { public class Room : NotifyPropertyChanged { private CancellationTokenSource _debounceCts = new(); private ObservableCollection<StateEventResponse> _timeline; - private ObservableDictionary<string,List<StateEventResponse>> _accountData; + private ObservableDictionary<string, List<StateEventResponse>> _accountData; public Room(string roomId) { if (string.IsNullOrWhiteSpace(roomId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(roomId)); @@ -98,12 +100,23 @@ public class RoomStore { set { if (Equals(value, _timeline)) return; _timeline = new(value); - _timeline.CollectionChanged += (sender, args) => SaveDebounced(); + _timeline.CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Add) { + foreach (StateEventResponse state in args.NewItems) { + if (state.StateKey is not null) + // we want state to be deduplicated by type and key, and we want the latest state to be the one that is returned + RebuildState(); + } + } + + SaveDebounced(); + }; + RebuildState(); OnPropertyChanged(); } } - public ObservableDictionary<string, List<StateEventResponse>> AccountData { + public ObservableDictionary<string, List<StateEventResponse>> AccountData { get => _accountData; set { if (Equals(value, _accountData)) return; @@ -113,6 +126,9 @@ public class RoomStore { } } + public ImmutableList<StateEventResponse> JoinedMembers => + State.Where(s => s is { Type: RoomMemberEventContent.EventId, TypedContent: RoomMemberEventContent { Membership: "join" } }).ToImmutableList(); + internal StateEventResponse SetStateInternal(StateEvent request) { var state = new StateEventResponse() { Type = request.Type, @@ -121,14 +137,16 @@ public class RoomStore { RoomId = RoomId, OriginServerTs = DateTimeOffset.Now.ToUnixTimeMilliseconds(), Sender = "", - RawContent = request.RawContent ?? (request.TypedContent is not null ? new JsonObject() : JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(request.TypedContent))) + RawContent = request.RawContent ?? (request.TypedContent is not null + ? new JsonObject() + : JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(request.TypedContent))) }; Timeline.Add(state); - if(state.StateKey is not null) - // we want state to be deduplicated by type and key, and we want the latest state to be the one that is returned - State = Timeline.Where(s => s.Type == state.Type && s.StateKey == state.StateKey) + if (state.StateKey is not null) + // we want state to be deduplicated by type and key, and we want the latest state to be the one that is returned + State = Timeline.Where(s => s.StateKey != null) .OrderByDescending(s => s.OriginServerTs) - .DistinctBy(x=>(x.Type, x.StateKey)) + .DistinctBy(x => (x.Type, x.StateKey)) .ToFrozenSet(); return state; } @@ -145,7 +163,7 @@ public class RoomStore { state.Sender = userId; return state; } - + public async Task SaveDebounced() { if (!HSEConfiguration.Current.StoreData) return; await _debounceCts.CancelAsync(); @@ -153,12 +171,20 @@ public class RoomStore { try { await Task.Delay(250, _debounceCts.Token); // Ensure all state events are in the timeline - State.Where(s=>!Timeline.Contains(s)).ToList().ForEach(s => Timeline.Add(s)); + State.Where(s => !Timeline.Contains(s)).ToList().ForEach(s => Timeline.Add(s)); var path = Path.Combine(HSEConfiguration.Current.DataStoragePath, "rooms", $"{RoomId}.json"); Console.WriteLine($"Saving room {RoomId} to {path}!"); await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true)); } catch (TaskCanceledException) { } } + + private void RebuildState() { + State = Timeline //.Where(s => s.Type == state.Type && s.StateKey == state.StateKey) + .Where(x => x.StateKey != null) + .OrderByDescending(s => s.OriginServerTs) + .DistinctBy(x => (x.Type, x.StateKey)) + .ToFrozenSet(); + } } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs index faf0046..4f238a0 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs @@ -187,7 +187,6 @@ public class UserStore { public class SyncRoomPosition { public int TimelinePosition { get; set; } - public int StatePosition { get; set; } public int AccountDataPosition { get; set; } } } |