about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--LibMatrix/EventIdResponse.cs4
-rw-r--r--LibMatrix/StateEvent.cs2
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs4
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs52
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs34
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs15
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs40
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs52
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs1
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; }
                 }
             }