diff options
Diffstat (limited to 'Tests/LibMatrix.HomeserverEmulator/Services')
5 files changed, 223 insertions, 50 deletions
diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs index e40af89..00f2a42 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs @@ -1,29 +1,20 @@ -using System.Collections.Concurrent; -using System.Collections.Frozen; -using System.Collections.ObjectModel; -using System.Security.Cryptography; using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using ArcaneLibs; -using ArcaneLibs.Collections; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Responses; +using LibMatrix.Services; namespace LibMatrix.HomeserverEmulator.Services; public class MediaStore { private readonly HSEConfiguration _config; + private readonly HomeserverResolverService _hsResolver; private List<MediaInfo> index = new(); - public MediaStore(HSEConfiguration config) { + public MediaStore(HSEConfiguration config, HomeserverResolverService hsResolver) { _config = config; + _hsResolver = hsResolver; if (config.StoreData) { var path = Path.Combine(config.DataStoragePath, "media"); if (!Directory.Exists(path)) Directory.CreateDirectory(path); - if(File.Exists(Path.Combine(path, "index.json"))) + if (File.Exists(Path.Combine(path, "index.json"))) index = JsonSerializer.Deserialize<List<MediaInfo>>(File.ReadAllText(Path.Combine(path, "index.json"))); } else @@ -31,17 +22,43 @@ public class MediaStore { } // public async Task<object> UploadMedia(string userId, string mimeType, Stream stream, string? filename = null) { - // var mediaId = $"mxc://{Guid.NewGuid().ToString()}"; - // var path = Path.Combine(_config.DataStoragePath, "media", mediaId); - // if (!Directory.Exists(path)) Directory.CreateDirectory(path); - // var file = Path.Combine(path, filename ?? "file"); - // await using var fs = File.Create(file); - // await stream.CopyToAsync(fs); - // index.Add(new() { - // - // }); - // return media; + // var mediaId = $"mxc://{Guid.NewGuid().ToString()}"; + // var path = Path.Combine(_config.DataStoragePath, "media", mediaId); + // if (!Directory.Exists(path)) Directory.CreateDirectory(path); + // var file = Path.Combine(path, filename ?? "file"); + // await using var fs = File.Create(file); + // await stream.CopyToAsync(fs); + // index.Add(new() { }); + // return media; // } + public async Task<Stream> GetRemoteMedia(string serverName, string mediaId) { + if (_config.StoreData) { + var path = Path.Combine(_config.DataStoragePath, "media", serverName, mediaId); + if (!File.Exists(path)) { + var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + if (mediaUrl is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "Media not found" + }; + using var client = new HttpClient(); + var stream = await client.GetStreamAsync(mediaUrl); + await using var fs = File.Create(path); + await stream.CopyToAsync(fs); + } + return new FileStream(path, FileMode.Open); + } + else { + var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + if (mediaUrl is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "Media not found" + }; + using var client = new HttpClient(); + return await client.GetStreamAsync(mediaUrl); + } + } public class MediaInfo { } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs b/Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs new file mode 100644 index 0000000..0128ba6 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs @@ -0,0 +1,54 @@ +namespace LibMatrix.HomeserverEmulator.Services; + +public class PaginationTokenResolverService(ILogger<PaginationTokenResolverService> logger, RoomStore roomStore, UserStore userStore) { + public async Task<long?> ResolveTokenToTimestamp(string token) { + logger.LogTrace("ResolveTokenToTimestamp({token})", token); + if (token.StartsWith('$')) { + //we have an event ID + foreach (var room in roomStore._rooms) { + var evt = await ResolveTokenToEvent(token, room); + if (evt is not null) return evt.OriginServerTs; + } + + // event not found + throw new NotImplementedException(); + } + else { + // we have a sync token + foreach (var user in userStore._users) { + foreach (var (_, session) in user.AccessTokens) { + if (!session.SyncStates.TryGetValue(token, out var syncState)) continue; + long? maxTs = 0; + foreach (var room in syncState.RoomPositions) { + var roomObj = roomStore.GetRoomById(room.Key); + if (roomObj is null) + continue; + var ts = roomObj.Timeline.Last().OriginServerTs; + if (ts > maxTs) maxTs = ts; + } + + return maxTs; + } + } + + throw new NotImplementedException(); + } + } + + public async Task<StateEventResponse?> ResolveTokenToEvent(string token, RoomStore.Room room) { + if (token.StartsWith('$')) { + //we have an event ID + logger.LogTrace("ResolveTokenToEvent(EventId({token}), Room({room})): searching for event...", token, room.RoomId); + + var evt = room.Timeline.SingleOrDefault(x => x.EventId == token); + if (evt is not null) return evt; + logger.LogTrace("ResolveTokenToEvent({token}, Room({room})): event not in requested room...", token, room.RoomId); + return null; + } + else { + // we have a sync token + logger.LogTrace("ResolveTokenToEvent(SyncToken({token}), Room({room}))", token, room.RoomId); + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs index d2c9e15..7b36f37 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs @@ -3,24 +3,24 @@ 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; -using System.Text.Json.Serialization; using ArcaneLibs; using ArcaneLibs.Collections; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes; using LibMatrix.EventTypes.Spec.State; +using LibMatrix.HomeserverEmulator.Controllers.Rooms; using LibMatrix.Responses; namespace LibMatrix.HomeserverEmulator.Services; public class RoomStore { + private readonly ILogger<RoomStore> _logger; public ConcurrentBag<Room> _rooms = new(); private Dictionary<string, Room> _roomsById = new(); - public RoomStore(HSEConfiguration config) { + public RoomStore(ILogger<RoomStore> logger, HSEConfiguration config) { + _logger = logger; if (config.StoreData) { var path = Path.Combine(config.DataStoragePath, "rooms"); if (!Directory.Exists(path)) Directory.CreateDirectory(path); @@ -50,8 +50,19 @@ public class RoomStore { return CreateRoom(new() { }); } - public Room CreateRoom(CreateRoomRequest request) { + public Room CreateRoom(CreateRoomRequest request, UserStore.User? user = null) { var room = new Room(roomId: $"!{Guid.NewGuid().ToString()}"); + var newCreateEvent = new StateEvent() { + Type = RoomCreateEventContent.EventId, + RawContent = new() { } + }; + foreach (var (key, value) in request.CreationContent) { + newCreateEvent.RawContent[key] = value.DeepClone(); + } + + if (user != null) newCreateEvent.RawContent["creator"] = user.UserId; + var createEvent = room.SetStateInternal(newCreateEvent); + if (!string.IsNullOrWhiteSpace(request.Name)) room.SetStateInternal(new StateEvent() { Type = RoomNameEventContent.EventId, @@ -77,23 +88,33 @@ public class RoomStore { return room; } + public Room AddRoom(Room room) { + _rooms.Add(room); + RebuildIndexes(); + + return room; + } + public class Room : NotifyPropertyChanged { private CancellationTokenSource _debounceCts = new(); private ObservableCollection<StateEventResponse> _timeline; private ObservableDictionary<string, List<StateEventResponse>> _accountData; + private ObservableDictionary<string, ReadMarkersData> _readMarkers; + private FrozenSet<StateEventResponse> _stateCache; + private int _timelineHash; public Room(string roomId) { if (string.IsNullOrWhiteSpace(roomId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(roomId)); if (roomId[0] != '!') throw new ArgumentException("Room ID must start with !", nameof(roomId)); RoomId = roomId; - State = FrozenSet<StateEventResponse>.Empty; Timeline = new(); AccountData = new(); + ReadMarkers = new(); } public string RoomId { get; set; } - public FrozenSet<StateEventResponse> State { get; private set; } + public FrozenSet<StateEventResponse> State => _timelineHash == _timeline.GetHashCode() ? _stateCache : RebuildState(); public ObservableCollection<StateEventResponse> Timeline { get => _timeline; @@ -129,11 +150,21 @@ public class RoomStore { public ImmutableList<StateEventResponse> JoinedMembers => State.Where(s => s is { Type: RoomMemberEventContent.EventId, TypedContent: RoomMemberEventContent { Membership: "join" } }).ToImmutableList(); + public ObservableDictionary<string, ReadMarkersData> ReadMarkers { + get => _readMarkers; + set { + if (Equals(value, _readMarkers)) return; + _readMarkers = new(value); + _readMarkers.CollectionChanged += (sender, args) => SaveDebounced(); + OnPropertyChanged(); + } + } + internal StateEventResponse SetStateInternal(StateEvent request) { var state = new StateEventResponse() { Type = request.Type, - StateKey = request.StateKey, - EventId = Guid.NewGuid().ToString(), + StateKey = request.StateKey ?? "", + EventId = "$" + Guid.NewGuid().ToString(), RoomId = RoomId, OriginServerTs = DateTimeOffset.Now.ToUnixTimeMilliseconds(), Sender = "", @@ -142,12 +173,12 @@ public class RoomStore { : 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.StateKey != null) - .OrderByDescending(s => s.OriginServerTs) - .DistinctBy(x => (x.Type, x.StateKey)) - .ToFrozenSet(); + // 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)) + // .ToFrozenSet(); return state; } @@ -179,12 +210,32 @@ public class RoomStore { catch (TaskCanceledException) { } } - private void RebuildState() { - State = Timeline //.Where(s => s.Type == state.Type && s.StateKey == state.StateKey) + private FrozenSet<StateEventResponse> RebuildState() { + foreach (var evt in Timeline) { + if (evt.EventId == null) + evt.EventId = "$" + Guid.NewGuid(); + else if (!evt.EventId.StartsWith('$')) { + evt.EventId = "$" + evt.EventId; + Console.WriteLine($"Sanitised invalid event ID {evt.EventId}"); + } + } + + _stateCache = 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(); + + return _stateCache; } } + + public List<StateEventResponse> GetRoomsByMember(string userId) { + // return _rooms + // // .Where(r => r.State.Any(s => s.Type == RoomMemberEventContent.EventId && s.StateKey == userId)) + // .Select(r => (Room: r, MemberEvent: r.State.SingleOrDefault(s => s.Type == RoomMemberEventContent.EventId && s.StateKey == userId))) + // .Where(r => r.MemberEvent != null) + // .ToDictionary(x => x.Room, x => x.MemberEvent!); + return _rooms.SelectMany(r => r.State.Where(s => s.Type == RoomMemberEventContent.EventId && s.StateKey == userId)).ToList(); + } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs index 1f59342..cf79aae 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs @@ -1,7 +1,7 @@ namespace LibMatrix.HomeserverEmulator.Services; public class TokenService{ - public string? GetAccessToken(HttpContext ctx) { + public string? GetAccessTokenOrNull(HttpContext ctx) { //qry if (ctx.Request.Query.TryGetValue("access_token", out var token)) { return token; @@ -16,6 +16,13 @@ public class TokenService{ return null; } + public string GetAccessToken(HttpContext ctx) { + return GetAccessTokenOrNull(ctx) ?? throw new MatrixException() { + ErrorCode = MatrixException.ErrorCodes.M_UNKNOWN_TOKEN, + Error = "Missing token" + }; + } + public string? GenerateServerName(HttpContext ctx) { return ctx.Request.Host.ToString(); } diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs index 4f238a0..4ce9f92 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs +++ b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs @@ -18,11 +18,15 @@ public class UserStore { public UserStore(HSEConfiguration config, RoomStore roomStore) { _roomStore = roomStore; if (config.StoreData) { - var path = Path.Combine(config.DataStoragePath, "users"); - if (!Directory.Exists(path)) Directory.CreateDirectory(path); - foreach (var file in Directory.GetFiles(path)) { - var user = JsonSerializer.Deserialize<User>(File.ReadAllText(file)); - if (user is not null) _users.Add(user); + var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users"); + if (!Directory.Exists(dataDir)) Directory.CreateDirectory(dataDir); + foreach (var userId in Directory.GetDirectories(dataDir)) { + var tokensDir = Path.Combine(dataDir, userId, "tokens.json"); + var path = Path.Combine(dataDir, userId, $"user.json"); + + var user = JsonSerializer.Deserialize<User>(File.ReadAllText(path)); + user!.AccessTokens = JsonSerializer.Deserialize<ObservableDictionary<string, User.SessionInfo>>(File.ReadAllText(tokensDir))!; + _users.Add(user); } Console.WriteLine($"Loaded {_users.Count} users from disk"); @@ -42,7 +46,7 @@ public class UserStore { return await CreateUser(userId); } - public async Task<User?> GetUserByToken(string token, bool createIfNotExists = false, string? serverName = null) { + public async Task<User?> GetUserByTokenOrNull(string token, bool createIfNotExists = false, string? serverName = null) { if (_users.Any(x => x.AccessTokens.ContainsKey(token))) return _users.First(x => x.AccessTokens.ContainsKey(token)); @@ -53,6 +57,13 @@ public class UserStore { return await CreateUser(uid); } + public async Task<User> GetUserByToken(string token, bool createIfNotExists = false, string? serverName = null) { + return await GetUserByTokenOrNull(token, createIfNotExists, serverName) ?? throw new MatrixException() { + ErrorCode = MatrixException.ErrorCodes.M_UNKNOWN_TOKEN, + Error = "Invalid token." + }; + } + public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null) { profile ??= new(); if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]); @@ -97,6 +108,7 @@ public class UserStore { Profile = new(); AccountData = new(); RoomKeys = new(); + AuthorizedSessions = new(); } private CancellationTokenSource _debounceCts = new(); @@ -106,6 +118,7 @@ public class UserStore { private ObservableDictionary<string, object> _profile; private ObservableCollection<StateEventResponse> _accountData; private ObservableDictionary<string, RoomKeysResponse> _roomKeys; + private ObservableDictionary<string, AuthorizedSession> _authorizedSessions; public string UserId { get => _userId; @@ -162,15 +175,29 @@ public class UserStore { } } + public ObservableDictionary<string, AuthorizedSession> AuthorizedSessions { + get => _authorizedSessions; + set { + if (value == _authorizedSessions) return; + _authorizedSessions = new(value); + _authorizedSessions.CollectionChanged += async (sender, args) => await SaveDebounced(); + OnPropertyChanged(); + } + } + public async Task SaveDebounced() { if (!HSEConfiguration.Current.StoreData) return; - _debounceCts.Cancel(); + await _debounceCts.CancelAsync(); _debounceCts = new CancellationTokenSource(); try { await Task.Delay(250, _debounceCts.Token); - var path = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users", $"{_userId}.json"); + var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users", _userId); + if (!Directory.Exists(dataDir)) Directory.CreateDirectory(dataDir); + var tokensDir = Path.Combine(dataDir, "tokens.json"); + var path = Path.Combine(dataDir, $"user.json"); Console.WriteLine($"Saving user {_userId} to {path}!"); await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true)); + await File.WriteAllTextAsync(tokensDir, AccessTokens.ToJson(ignoreNull: true)); } catch (TaskCanceledException) { } catch (InvalidOperationException) { } // We don't care about 100% data safety, this usually happens when something is updated while serialising @@ -187,7 +214,19 @@ public class UserStore { public class SyncRoomPosition { public int TimelinePosition { get; set; } + public string LastTimelineEventId { get; set; } public int AccountDataPosition { get; set; } + public bool Joined { get; set; } + } + + public UserSyncState Clone() { + return new() { + FilterId = FilterId, + RoomPositions = RoomPositions.ToDictionary(x => x.Key, x => new SyncRoomPosition() { + TimelinePosition = x.Value.TimelinePosition, + AccountDataPosition = x.Value.AccountDataPosition + }) + }; } } } @@ -202,5 +241,10 @@ public class UserStore { UserId = UserId }; } + + public class AuthorizedSession { + public string Homeserver { get; set; } + public string AccessToken { get; set; } + } } } \ No newline at end of file |