diff options
18 files changed, 607 insertions, 4 deletions
diff --git a/LibMatrix.sln b/LibMatrix.sln index f3eae7d..c068216 100644 --- a/LibMatrix.sln +++ b/LibMatrix.sln @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\Ar EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{CD13665B-B964-4AB0-991B-12F067B16DA3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,6 +94,10 @@ Global {CD13665B-B964-4AB0-991B-12F067B16DA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD13665B-B964-4AB0-991B-12F067B16DA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD13665B-B964-4AB0-991B-12F067B16DA3}.Release|Any CPU.Build.0 = Release|Any CPU + {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D} @@ -103,5 +109,6 @@ Global {0B9B34D1-9362-45A9-9C21-816FD6959110} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} {4D9B5227-48DC-4A30-9263-AFB51DC01ABB} = {A6345ECE-4C5E-400F-9130-886E343BF314} {13A797D1-7E13-4789-A167-8628B1641AC0} = {01A126FE-9D50-40F2-817B-E55F4065EA76} + {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} EndGlobalSection EndGlobal diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index 5c7d254..422a8a9 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -35,7 +35,7 @@ public class RemoteHomeserver(string baseUrl) { homeserver.ClientHttpClient = new MatrixHttpClient { BaseAddress = new Uri(proxy ?? homeserver.WellKnownUris.Client ?? throw new InvalidOperationException($"Failed to resolve homeserver client URI for {baseUrl}")), - Timeout = TimeSpan.FromSeconds(120) + Timeout = TimeSpan.FromSeconds(300) }; homeserver.FederationClient = await FederationClient.TryCreate(baseUrl, proxy); diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs index 86dbce4..8ec8fd5 100644 --- a/LibMatrix/MatrixException.cs +++ b/LibMatrix/MatrixException.cs @@ -20,7 +20,7 @@ public class MatrixException : Exception { public string RawContent { get; set; } - public object GetAsObject() => new { ErrorCode, Error, SoftLogout, RetryAfterMs }; + public object GetAsObject() => new { errcode = ErrorCode, error = Error, soft_logout = SoftLogout, retry_after_ms = RetryAfterMs }; public string GetAsJson() => GetAsObject().ToJson(ignoreNull: true); public override string Message => diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index a4a18e5..bcef541 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -34,7 +34,14 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge } private async Task<string?> _tryResolveFromClientWellknown(string homeserver) { - if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; + if (!homeserver.StartsWith("http")) { + if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/.well-known/matrix/client")) + homeserver = "https://" + homeserver; + else if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/.well-known/matrix/client")) { + homeserver = "http://" + homeserver; + } + } + try { var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client"); var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString(); @@ -49,7 +56,14 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge } private async Task<string?> _tryResolveFromServerWellknown(string homeserver) { - if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; + if (!homeserver.StartsWith("http")) { + if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/.well-known/matrix/server")) + homeserver = "https://" + homeserver; + else if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/.well-known/matrix/server")) { + homeserver = "http://" + homeserver; + } + } + try { var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/server"); var hs = resp.GetProperty("m.server").GetString(); diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs new file mode 100644 index 0000000..d0496bf --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Nodes; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/client/{version}/")] +public class AuthController(ILogger<AuthController> logger, UserStore userStore) : ControllerBase { + [HttpPost("login")] + public async Task<LoginResponse> Login(LoginRequest request) { + var user = await userStore.CreateUser($"@{Guid.NewGuid().ToString()}:{Request.Host}", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary<string, object>()); + var loginResponse = new LoginResponse { + AccessToken = user.AccessToken, + DeviceId = user.DeviceId, + UserId = user.UserId + }; + + return loginResponse; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs new file mode 100644 index 0000000..0c4d8bd --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs @@ -0,0 +1,18 @@ +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_hsEmulator")] +public class HEDebugController(ILogger<HEDebugController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("users")] + public async Task<List<UserStore.User>> GetUsers() { + return userStore._users; + } + + [HttpGet("rooms")] + public async Task<List<RoomStore.Room>> GetRooms() { + return roomStore._rooms.ToList(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs new file mode 100644 index 0000000..d763b26 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/UserController.cs @@ -0,0 +1,81 @@ +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/client/{version}/")] +public class UserController(ILogger<UserController> logger, TokenService tokenService, UserStore userStore) : ControllerBase { + [HttpGet("account/whoami")] + public async Task<WhoAmIResponse> Login() { + var token = tokenService.GetAccessToken(); + if (token is null) + throw new MatrixException() { + ErrorCode = "M_UNAUTHORIZED", + Error = "No token passed." + }; + + var user = await userStore.GetUserByToken(token, Random.Shared.Next(101) <= 10, tokenService.GenerateServerName()); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "Invalid token." + }; + var whoAmIResponse = new WhoAmIResponse { + UserId = user.UserId + }; + return whoAmIResponse; + } + + [HttpGet("profile/{userId}")] + public async Task<Dictionary<string, object>> GetProfile(string userId) { + var user = await userStore.GetUserById(userId, false); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "User not found." + }; + return user.Profile; + } + + [HttpGet("profile/{userId}/{key}")] + public async Task<object> GetProfile(string userId, string key) { + var user = await userStore.GetUserById(userId, false); + if (user is null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "User not found." + }; + if (!user.Profile.TryGetValue(key, out var value)) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "Key not found." + }; + return value; + } + + [HttpGet("joined_rooms")] + public async Task<object> GetJoinedRooms() { + var token = tokenService.GetAccessToken(); + 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 user.JoinedRooms; + + return new { + joined_rooms = user.JoinedRooms + }; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs new file mode 100644 index 0000000..1349fac --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Nodes; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/")] +public class VersionsController(ILogger<WellKnownController> logger) : ControllerBase { + [HttpGet("client/versions")] + public async Task<ClientVersionsResponse> GetClientVersions() { + var clientVersions = new ClientVersionsResponse { + Versions = new() { + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2", + "v1.3", + "v1.4", + "v1.5", + "v1.6", + "v1.7", + "v1.8", + }, + UnstableFeatures = new() + }; + return clientVersions; + } + + [HttpGet("federation/v1/version")] + public async Task<ServerVersionResponse> GetServerVersions() { + var clientVersions = new ServerVersionResponse() { + Server = new() { + Name = "LibMatrix.HomeserverEmulator", + Version = "0.0.0" + } + }; + return clientVersions; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs new file mode 100644 index 0000000..97e460d --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Nodes; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/.well-known/matrix/")] +public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase { + [HttpGet("client")] + public JsonObject GetClientWellKnown() { + var obj = new JsonObject() { + ["m.homeserver"] = new JsonObject() { + ["base_url"] = $"{Request.Scheme}://{Request.Host}" + } + }; + + logger.LogInformation("Serving client well-known: {}", obj); + + return obj; + } + [HttpGet("server")] + public JsonObject GetServerWellKnown() { + var obj = new JsonObject() { + ["m.server"] = $"{Request.Scheme}://{Request.Host}" + }; + + logger.LogInformation("Serving server well-known: {}", obj); + + return obj; + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj new file mode 100644 index 0000000..e6b4572 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <InvariantGlobalization>true</InvariantGlobalization> + <LangVersion>preview</LangVersion> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="EasyCompressor.LZMA" Version="1.4.0" /> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> +</Project> diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http new file mode 100644 index 0000000..01d5fcc --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http @@ -0,0 +1,6 @@ +@LibMatrix.HomeserverEmulator_HostAddress = http://localhost:5298 + +GET {{LibMatrix.HomeserverEmulator_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Tests/LibMatrix.HomeserverEmulator/Program.cs b/Tests/LibMatrix.HomeserverEmulator/Program.cs new file mode 100644 index 0000000..516d380 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Program.cs @@ -0,0 +1,90 @@ +using System.Net.Mime; +using LibMatrix; +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Timeouts; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => { + c.SwaggerDoc("v1", new OpenApiInfo() { + Version = "v1", + Title = "Rory&::LibMatrix.HomeserverEmulator", + Description = "Partial Matrix implementation" + }); + c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "LibMatrix.HomeserverEmulator.xml")); +}); +builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); +builder.Services.AddSingleton<UserStore>(); +builder.Services.AddSingleton<RoomStore>(); + + +builder.Services.AddScoped<TokenService>(); + +builder.Services.AddRequestTimeouts(x => { + x.DefaultPolicy = new RequestTimeoutPolicy { + Timeout = TimeSpan.FromMinutes(10), + WriteTimeoutResponse = async context => { + context.Response.StatusCode = 504; + context.Response.ContentType = "application/json"; + await context.Response.StartAsync(); + await context.Response.WriteAsJsonAsync(new MatrixException() { + ErrorCode = "M_TIMEOUT", + Error = "Request timed out" + }.GetAsJson()); + await context.Response.CompleteAsync(); + } + }; +}); +builder.Services.AddCors(options => { + options.AddPolicy( + "Open", + policy => policy.AllowAnyOrigin().AllowAnyHeader()); +}); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment() || true) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("Open"); +app.UseExceptionHandler(exceptionHandlerApp => { + exceptionHandlerApp.Run(async context => { + + var exceptionHandlerPathFeature = + context.Features.Get<IExceptionHandlerPathFeature>(); + + if (exceptionHandlerPathFeature?.Error is MatrixException mxe) { + context.Response.StatusCode = mxe.ErrorCode switch { + "M_NOT_FOUND" => StatusCodes.Status404NotFound, + "M_UNAUTHORIZED" => StatusCodes.Status401Unauthorized, + _ => StatusCodes.Status500InternalServerError + }; + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(mxe.GetAsJson()!); + } + else { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(new MatrixException() { + ErrorCode = "M_UNKNOWN", + Error = exceptionHandlerPathFeature?.Error.ToString() + }.GetAsJson()); + } + }); +}); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json b/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json new file mode 100644 index 0000000..8ab6b3d --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6824", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5298", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs new file mode 100644 index 0000000..b4624ab --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs @@ -0,0 +1,83 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using LibMatrix.EventTypes; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Responses; + +namespace LibMatrix.HomeserverEmulator.Services; + +public class RoomStore { + public ConcurrentBag<Room> _rooms = new(); + private Dictionary<string, Room> _roomsById = new(); + + private void RebuildIndexes() { + _roomsById = _rooms.ToDictionary(u => u.RoomId); + } + + public Room? GetRoomById(string roomId, bool createIfNotExists = false) { + if (_roomsById.TryGetValue(roomId, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + + return CreateRoom(new() { }); + } + + public Room CreateRoom(CreateRoomRequest request) { + var room = new Room { + RoomId = $"!{Guid.NewGuid().ToString()}" + }; + if (!string.IsNullOrWhiteSpace(request.Name)) + room.SetStateInternal(new StateEvent() { + Type = RoomNameEventContent.EventId, + TypedContent = new RoomNameEventContent() { + Name = request.Name + } + }); + + if (!string.IsNullOrWhiteSpace(request.RoomAliasName)) + room.SetStateInternal(new StateEvent() { + Type = RoomCanonicalAliasEventContent.EventId, + TypedContent = new RoomCanonicalAliasEventContent() { + Alias = $"#{request.RoomAliasName}:localhost" + } + }); + + foreach (var stateEvent in request.InitialState ?? []) { + room.SetStateInternal(stateEvent); + } + + _rooms.Add(room); + RebuildIndexes(); + return room; + } + + public class Room { + public string RoomId { get; set; } + public List<StateEventResponse> State { get; set; } = new(); + public Dictionary<string, EventContent> Timeline { get; set; } = new(); + + internal StateEventResponse SetStateInternal(StateEvent request) { + var state = new StateEventResponse() { + Type = request.Type, + StateKey = request.StateKey, + RawContent = request.RawContent, + EventId = Guid.NewGuid().ToString() + }; + State.Add(state); + return state; + } + + public StateEventResponse AddUser(string userId) { + return SetStateInternal(new() { + Type = RoomMemberEventContent.EventId, + StateKey = userId, + TypedContent = new RoomMemberEventContent() { + Membership = "join" + } + }); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs new file mode 100644 index 0000000..8115bee --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs @@ -0,0 +1,26 @@ +namespace LibMatrix.HomeserverEmulator.Services; + +public class TokenService(IHttpContextAccessor accessor) { + public string? GetAccessToken() { + var ctx = accessor.HttpContext; + if (ctx is null) return null; + //qry + if (ctx.Request.Query.TryGetValue("access_token", out var token)) { + return token; + } + //header + if (ctx.Request.Headers.TryGetValue("Authorization", out var auth)) { + var parts = auth.ToString().Split(' '); + if (parts.Length == 2 && parts[0] == "Bearer") { + return parts[1]; + } + } + return null; + } + + public string? GenerateServerName() { + var ctx = accessor.HttpContext; + if (ctx is null) return null; + return ctx.Request.Host.ToString(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs new file mode 100644 index 0000000..ca1c577 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs @@ -0,0 +1,72 @@ +using LibMatrix.EventTypes.Spec.State; + +namespace LibMatrix.HomeserverEmulator.Services; + +public class UserStore(RoomStore roomStore) { + public List<User> _users = new(); + private Dictionary<string, User> _usersById = new(); + private Dictionary<string, User> _usersByToken = new(); + + private void RebuildIndexes() { + _usersById = _users.ToDictionary(u => u.UserId); + _usersByToken = _users.ToDictionary(u => u.AccessToken); + } + + public async Task<User?> GetUserById(string userId, bool createIfNotExists = false) { + if (_usersById.TryGetValue(userId, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + + return await CreateUser(userId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new Dictionary<string, object>()); + } + + public async Task<User?> GetUserByToken(string token, bool createIfNotExists = false, string? serverName = null) { + if (_usersByToken.TryGetValue(token, out var user)) { + return user; + } + + if (!createIfNotExists) + return null; + if (string.IsNullOrWhiteSpace(serverName)) throw new NullReferenceException("Server name was not passed"); + var uid = $"@{Guid.NewGuid().ToString()}:{serverName}"; + return await CreateUser(uid, Guid.NewGuid().ToString(), token, new Dictionary<string, object>()); + } + + public async Task<User> CreateUser(string userId, string deviceId, string accessToken, Dictionary<string, object> profile) { + if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]); + if (!profile.ContainsKey("avatar_url")) profile.Add("avatar_url", null); + var user = new User { + UserId = userId, + DeviceId = deviceId, + AccessToken = accessToken, + Profile = profile + }; + _users.Add(user); + RebuildIndexes(); + + if (roomStore._rooms.Count > 0) + foreach (var item in Random.Shared.GetItems(roomStore._rooms.ToArray(), Math.Min(roomStore._rooms.Count, 400))) { + item.AddUser(userId); + } + + int random = Random.Shared.Next(10); + for (int i = 0; i < random; i++) { + var room = roomStore.CreateRoom(new()); + room.AddUser(userId); + } + + return user; + } + + public class User { + public string UserId { get; set; } + public string AccessToken { get; set; } + public string DeviceId { get; set; } + public Dictionary<string, object> Profile { get; set; } + + public List<string> JoinedRooms { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json b/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json new file mode 100644 index 0000000..df83ec5 --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Routing": "Warning", + "Microsoft.AspNetCore.Mvc": "Warning" + } + } +} diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.json b/Tests/LibMatrix.HomeserverEmulator/appsettings.json new file mode 100644 index 0000000..b16968a --- /dev/null +++ b/Tests/LibMatrix.HomeserverEmulator/appsettings.json @@ -0,0 +1,41 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + // Configuration for the proxy + "MxApiExtensions": { + // WARNING: this exposes user tokens to servers listed here, which could be a security risk + // Only list servers you trust! + // Keep in mind that token conflicts can occur between servers! + "AuthHomeservers": [ + "rory.gay", + "conduit.rory.gay" + ], + // List of administrator MXIDs for the proxy, this allows them to use administrative and debug endpoints + "Admins": [ + "@emma:rory.gay", + "@emma:conduit.rory.gay" + ], + "FastInitialSync": { + "Enabled": true, + "UseRoomInfoCache": true + }, + "Cache": { + "RoomInfo": { + "BaseTtl": "00:01:00", + "ExtraTtlPerState": "00:00:00.1000000" + } + }, + "DefaultUserConfiguration": { + "ProtocolChanges": { + "DisableThreads": false, + "DisableVoip": false, + "AutoFollowTombstones": false + } + } + } +} |