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