diff options
Diffstat (limited to 'Utilities')
8 files changed, 211 insertions, 17 deletions
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs index 66548e2..5550c26 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using System.Text.Json.Nodes; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Responses; @@ -8,7 +9,7 @@ namespace LibMatrix.HomeserverEmulator.Controllers; [ApiController] [Route("/_matrix/client/{version}/")] -public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService) : ControllerBase { +public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService, HSEConfiguration config) : ControllerBase { [HttpPost("login")] public async Task<LoginResponse> Login(LoginRequest request) { if (!request.Identifier.User.StartsWith('@')) @@ -58,6 +59,79 @@ public class AuthController(ILogger<AuthController> logger, UserStore userStore, user.AccessTokens.Remove(token); return new { }; } + + [HttpPost("register")] + public async Task<object> Register(JsonObject request, [FromQuery] string kind = "user") { + if (kind == "guest") { + var user = await userStore.CreateUser(Random.Shared.NextInt64(long.MaxValue).ToString(), kind: "guest"); + return user.Login(); + } + + if (request.Count == 0) { + return new { + session = Guid.NewGuid().ToString(), + flows = new { + stages = new[] { + "m.login.dummy", + } + } + }; + } + + if (request.ContainsKey("password")) { + var parts = request["username"].ToString().Split(':'); + var localpart = parts[0].TrimStart('@'); + var user = await userStore.CreateUser($"@{localpart}:{config.ServerName}"); + var login = user.Login(); + + if (request.ContainsKey("initial_device_display_name")) + user.AccessTokens[login.AccessToken].DeviceName = request["initial_device_display_name"]!.ToString(); + + return login; + } + + return new { }; + } + + [HttpGet("register/available")] + public async Task<object> IsUsernameAvailable([FromQuery] string username) { + return new { + available = await userStore.GetUserById($"@{username}:{config.ServerName}") is null + }; + } + + // [HttpPost("account/deactivate")] + // public async Task<object> DeactivateAccount() { + // var token = tokenService.GetAccessToken(HttpContext); + // var user = await userStore.GetUserByToken(token); + // if (user == null) + // throw new MatrixException() { + // ErrorCode = "M_UNKNOWN_TOKEN", + // Error = "No such user" + // }; + // + // + // return new { }; + // } + + #region 3PID + + [HttpGet("account/3pid")] + public async Task<object> Get3pid() { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + if (user == null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "No such user" + }; + + return new { + threepids = (object[])[] + }; + } + + #endregion } public class LoginFlowsResponse { diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs index 52d5932..b29edf5 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Homeservers; @@ -12,6 +13,8 @@ namespace LibMatrix.HomeserverEmulator.Controllers; [ApiController] [Route("/_matrix/")] public class DirectoryController(ILogger<DirectoryController> logger, RoomStore roomStore) : ControllerBase { +#region Room directory + [HttpGet("client/v3/directory/room/{alias}")] public async Task<AliasResult> GetRoomAliasV3(string alias) { var match = roomStore._rooms.FirstOrDefault(x => @@ -31,4 +34,34 @@ public class DirectoryController(ILogger<DirectoryController> logger, RoomStore Servers = servers }; } + +#endregion + +#region User directory + + [HttpPost("client/v3/user_directory/search")] + public async Task<UserDirectoryResponse> SearchUserDirectory([FromBody] UserDirectoryRequest request) { + var users = roomStore._rooms + .SelectMany(x => x.State.Where(y => + y.Type == RoomMemberEventContent.EventId + && y.RawContent?["membership"]?.ToString() == "join" + && (y.StateKey!.ContainsAnyCase(request.SearchTerm) || y.RawContent?["displayname"]?.ToString()?.ContainsAnyCase(request.SearchTerm) == true) + ) + ) + .DistinctBy(x => x.StateKey) + .ToList(); + + request.Limit ??= 10; + + return new() { + Results = users.Select(x => new UserDirectoryResponse.UserDirectoryResult { + UserId = x.StateKey!, + DisplayName = x.RawContent?["displayname"]?.ToString(), + AvatarUrl = x.RawContent?["avatar_url"]?.ToString() + }).ToList(), + Limited = users.Count > request.Limit + }; + } + +#endregion } \ No newline at end of file diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs index 7898a8c..18cccf6 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs @@ -11,6 +11,25 @@ // [ApiController] // [Route("/_matrix/client/{version}/")] // public class KeysController(ILogger<KeysController> logger, TokenService tokenService, UserStore userStore) : ControllerBase { +// [HttpPost("keys/upload")] +// public async Task<object> UploadKeys(DeviceKeysUploadRequest 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 { }; +// } +// // [HttpGet("room_keys/version")] // public async Task<RoomKeysResponse> GetRoomKeys() { // var token = tokenService.GetAccessToken(HttpContext); diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs index 7899ada..59d37ff 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs @@ -42,6 +42,9 @@ public class MediaController( [HttpGet("download/{serverName}/{mediaId}")] public async Task DownloadMedia(string serverName, string mediaId) { + Response.Headers["Access-Control-Allow-Origin"] = "*"; + Response.Headers["Access-Control-Allow-Methods"] = "GET"; + var stream = await DownloadRemoteMedia(serverName, mediaId); await stream.CopyToAsync(Response.Body); } @@ -56,6 +59,7 @@ public class MediaController( JsonObject data = new(); using var hc = new HttpClient(); + logger.LogInformation("Getting URL preview for {}", url); using var response = await hc.GetAsync(url); var doc = await response.Content.ReadAsStringAsync(); var match = Regex.Match(doc, "<meta property=\"(.*?)\" content=\"(.*?)\""); @@ -80,6 +84,7 @@ public class MediaController( }; using var client = new HttpClient(); var stream = await client.GetStreamAsync(mediaUrl); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); await using var fs = System.IO.File.Create(path); await stream.CopyToAsync(fs); } diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs index afd69d1..7a16ace 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Text.Json.Nodes; using ArcaneLibs; +using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Helpers; @@ -46,7 +47,7 @@ public class RoomTimelineController( room.Timeline.Add(evt); if (evt.Type == RoomMessageEventContent.EventId && (evt.TypedContent as RoomMessageEventContent).Body.StartsWith("!hse")) - await HandleHseCommand(evt, room, user); + _ = Task.Run(() => HandleHseCommand(evt, room, user)); // else return new() { @@ -124,9 +125,10 @@ public class RoomTimelineController( return evt; } - + [HttpGet("relations/{eventId}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null, + [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -156,9 +158,10 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + [HttpGet("relations/{eventId}/{relationType}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b", + [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -188,9 +191,10 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + [HttpGet("relations/{eventId}/{relationType}/{eventType}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b", + [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -220,7 +224,7 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + private async Task<IEnumerable<StateEventResponse>> GetRelationsInternal(string roomId, string eventId, string dir, string? from, int? limit, bool? recurse, string? to) { var room = roomStore.GetRoomById(roomId); var evt = room.Timeline.SingleOrDefault(x => x.EventId == eventId); @@ -237,7 +241,7 @@ public class RoomTimelineController( else if (dir == "f") { relatedEvents = relatedEvents.Take(limit ?? 100); } - + return relatedEvents; } @@ -260,12 +264,14 @@ public class RoomTimelineController( } private async Task HandleHseCommand(StateEventResponse evt, RoomStore.Room room, UserStore.User user) { + logger.LogWarning("Handling HSE command for {0}: {1}", user.UserId, evt.RawContent.ToJson(false, true)); try { var msgContent = evt.TypedContent as RoomMessageEventContent; var parts = msgContent.Body.Split('\n')[0].Split(" "); if (parts.Length < 2) return; var command = parts[1]; + Console.WriteLine($"Handling command {command}"); switch (command) { case "import": await HandleImportCommand(parts[2..], evt, room, user); @@ -306,6 +312,7 @@ public class RoomTimelineController( } while (Process.GetCurrentProcess().WorkingSet64 >= 1_024_000_000); } } + break; } case "genrooms": { @@ -334,9 +341,11 @@ public class RoomTimelineController( }.ToStateEvent(user, room)); } } + var newRoom = roomStore.CreateRoom(crq); newRoom.AddUser(user.UserId); } + InternalSendMessage(room, $"Generated {count} new rooms in {sw.Elapsed}!"); break; } @@ -348,6 +357,21 @@ public class RoomTimelineController( InternalSendMessage(room, $"GC memory: {Util.BytesToString(GC.GetTotalMemory(false))}, total process memory: {Util.BytesToString(Process.GetCurrentProcess().WorkingSet64)}"); break; + case "leave-all-rooms": { + var rooms = roomStore.GetRoomsByMember(user.UserId); + foreach (var memberEvt in rooms) { + var roomObj = roomStore.GetRoomById(memberEvt.RoomId); + roomObj.SetStateInternal(new() { + Type = RoomMemberEventContent.EventId, + StateKey = user.UserId, + TypedContent = new RoomMemberEventContent() { + Membership = "leave" + }, + }, senderId: user.UserId); + } + + break; + } default: InternalSendMessage(room, $"Command {command} not found!"); break; diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs index 024d071..f585eed 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs @@ -28,10 +28,9 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe UserStore.User.SessionInfo.UserSyncState newSyncState = new(); SyncResponse syncResp; - if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.ContainsKey(since)) + if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.TryGetValue(since, out var syncState)) syncResp = InitialSync(user, session); else { - var syncState = session.SyncStates[since]; newSyncState = syncState.Clone(); var newSyncToken = Guid.NewGuid().ToString(); @@ -155,7 +154,10 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe } } - if (data.Join.Count > 0) return data; + if (data.Join.Count > 0) { + logger.LogTrace("Found {count} updated rooms", data.Join.Count); + return data; + } // step 2: check newly joined rooms var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList(); @@ -206,6 +208,10 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe #endregion + private bool HasData(SyncResponse resp) { + return resp.Rooms?.Invite?.Count > 0 || resp.Rooms?.Join?.Count > 0 || resp.Rooms?.Leave?.Count > 0; + } + private async Task<bool> HasDataOrStall(SyncResponse resp) { // logger.LogTrace("Checking if sync response has data: {resp}", resp.ToJson(indent: false, ignoreNull: true)); // if (resp.AccountData?.Events?.Count > 0) return true; @@ -256,6 +262,14 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe ToDevice: { Events: { Count: > 0 } } + } or { + Rooms: { + Invite: { Count: > 0 } + } or { + Join: { Count: > 0 } + } or { + Leave: { Count: > 0 } + } }; if (!hasData) { diff --git a/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs index 73b0d23..bcfb629 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs @@ -27,6 +27,8 @@ public class HSEConfiguration { public string DataStoragePath { get; set; } public bool StoreData { get; set; } = true; + + public string ServerName { get; set; } = "localhost"; public bool UnknownSyncTokenIsInitialSync { get; set; } = true; diff --git a/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs index 4ce9f92..4684b01 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs @@ -13,9 +13,11 @@ namespace LibMatrix.HomeserverEmulator.Services; public class UserStore { public ConcurrentBag<User> _users = new(); + private readonly HSEConfiguration _config; private readonly RoomStore _roomStore; public UserStore(HSEConfiguration config, RoomStore roomStore) { + _config = config; _roomStore = roomStore; if (config.StoreData) { var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users"); @@ -64,12 +66,15 @@ public class UserStore { }; } - public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null) { + public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null, string kind = "user") { profile ??= new(); - if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]); + var parts = userId.Split(":"); + var localPart = parts[0].TrimStart('@'); + if (!profile.ContainsKey("displayname")) profile.Add("displayname", localPart); if (!profile.ContainsKey("avatar_url")) profile.Add("avatar_url", null); var user = new User() { - UserId = userId, + UserId = $"@{localPart}:{_config.ServerName}", + IsGuest = kind == "guest", AccountData = new() { new StateEventResponse() { Type = "im.vector.analytics", @@ -80,7 +85,22 @@ public class UserStore { new StateEventResponse() { Type = "im.vector.web.settings", RawContent = new JsonObject() { - ["developerMode"] = true + ["developerMode"] = true, + ["alwaysShowTimestamps"] = true, + ["SpotlightSearch.showNsfwPublicRooms"] = true, + + } + }, + new() { + Type = "im.vector.setting.integration_provisioning", + RawContent = new JsonObject() { + ["enabled"] = false + } + }, + new() { + Type = "m.identity_server", + RawContent = new JsonObject() { + ["base_url"] = null } }, } @@ -185,6 +205,8 @@ public class UserStore { } } + public bool IsGuest { get; set; } + public async Task SaveDebounced() { if (!HSEConfiguration.Current.StoreData) return; await _debounceCts.CancelAsync(); @@ -205,6 +227,7 @@ public class UserStore { public class SessionInfo { public string DeviceId { get; set; } = Guid.NewGuid().ToString(); + public string DeviceName { get; set; } = "Unnamed device"; public Dictionary<string, UserSyncState> SyncStates { get; set; } = new(); public class UserSyncState { |