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 {
|