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; }
}
}
|