diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
index 4f10601..d0eaed4 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
@@ -12,13 +12,10 @@ public class AuthController(ILogger<AuthController> logger, UserStore userStore,
public async Task<LoginResponse> Login(LoginRequest request) {
if (!request.Identifier.User.StartsWith('@'))
request.Identifier.User = $"@{request.Identifier.User}:{tokenService.GenerateServerName(HttpContext)}";
- if (request.Identifier.User.EndsWith("localhost"))
- request.Identifier.User = request.Identifier.User.Replace("localhost", tokenService.GenerateServerName(HttpContext));
+ // if (request.Identifier.User.EndsWith("localhost"))
+ // request.Identifier.User = request.Identifier.User.Replace("localhost", tokenService.GenerateServerName(HttpContext));
- var user = await userStore.GetUserById(request.Identifier.User);
- if (user is null) {
- user = await userStore.CreateUser(request.Identifier.User);
- }
+ var user = await userStore.GetUserById(request.Identifier.User) ?? await userStore.CreateUser(request.Identifier.User);
return user.Login();
}
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
index 2e232d9..d497ca6 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
using ArcaneLibs.Extensions;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.HomeserverEmulator.Services;
@@ -32,6 +33,70 @@ public class DirectoryController(ILogger<DirectoryController> logger, RoomStore
};
}
+ [HttpGet("client/v3/publicRooms")]
+ public async Task<PublicRoomDirectoryResult> GetPublicRooms(int limit = 100, string? server = null, string? since = null) {
+ var rooms = roomStore._rooms.OrderByDescending(x => x.JoinedMembers.Count).AsEnumerable();
+
+ if (since != null) {
+ rooms = rooms.SkipWhile(x => x.RoomId != since).Skip(1);
+ }
+
+ if (server != null) {
+ rooms = rooms.Where(x => x.State.Any(y => y.Type == RoomMemberEventContent.EventId && y.StateKey!.EndsWith(server)));
+ }
+
+ var count = rooms.Count();
+ rooms = rooms.Take(limit);
+
+ return new PublicRoomDirectoryResult() {
+ Chunk = rooms.Select(x => new PublicRoomDirectoryResult.PublicRoomListItem() {
+ RoomId = x.RoomId,
+ Name = x.State.FirstOrDefault(y => y.Type == RoomNameEventContent.EventId)?.RawContent?["name"]?.ToString(),
+ Topic = x.State.FirstOrDefault(y => y.Type == RoomTopicEventContent.EventId)?.RawContent?["topic"]?.ToString(),
+ AvatarUrl = x.State.FirstOrDefault(y => y.Type == RoomAvatarEventContent.EventId)?.RawContent?["url"]?.ToString(),
+ GuestCanJoin = x.State.Any(y => y.Type == RoomGuestAccessEventContent.EventId && y.RawContent?["guest_access"]?.ToString() == "can_join"),
+ NumJoinedMembers = x.JoinedMembers.Count,
+ WorldReadable = x.State.Any(y => y.Type == RoomHistoryVisibilityEventContent.EventId && y.RawContent?["history_visibility"]?.ToString() == "world_readable"),
+ JoinRule = x.State.FirstOrDefault(y => y.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.ToString(),
+ CanonicalAlias = x.State.FirstOrDefault(y => y.Type == RoomCanonicalAliasEventContent.EventId)?.RawContent?["alias"]?.ToString()
+ }).ToList(),
+ NextBatch = count > limit ? rooms.Last().RoomId : null,
+ TotalRoomCountEstimate = count
+ };
+ }
+
+ [HttpPost("client/v3/publicRooms")]
+ public async Task<PublicRoomDirectoryResult> GetFilteredPublicRooms([FromBody] PublicRoomDirectoryRequest request, [FromQuery] string? server = null) {
+ var rooms = roomStore._rooms.OrderByDescending(x => x.JoinedMembers.Count).AsEnumerable();
+
+ if (request.Since != null) {
+ rooms = rooms.SkipWhile(x => x.RoomId != request.Since).Skip(1);
+ }
+
+ if (server != null) {
+ rooms = rooms.Where(x => x.State.Any(y => y.Type == RoomMemberEventContent.EventId && y.StateKey!.EndsWith(server)));
+ }
+
+ var count = rooms.Count();
+ rooms = rooms.Take(request.Limit ?? 100);
+
+ return new PublicRoomDirectoryResult() {
+ Chunk = rooms.Select(x => new PublicRoomDirectoryResult.PublicRoomListItem() {
+ RoomId = x.RoomId,
+ Name = x.State.FirstOrDefault(y => y.Type == RoomNameEventContent.EventId)?.RawContent?["name"]?.ToString(),
+ Topic = x.State.FirstOrDefault(y => y.Type == RoomTopicEventContent.EventId)?.RawContent?["topic"]?.ToString(),
+ AvatarUrl = x.State.FirstOrDefault(y => y.Type == RoomAvatarEventContent.EventId)?.RawContent?["url"]?.ToString(),
+ GuestCanJoin = x.State.Any(y => y.Type == RoomGuestAccessEventContent.EventId && y.RawContent?["guest_access"]?.ToString() == "can_join"),
+ NumJoinedMembers = x.JoinedMembers.Count,
+ WorldReadable = x.State.Any(y => y.Type == RoomHistoryVisibilityEventContent.EventId && y.RawContent?["history_visibility"]?.ToString() == "world_readable"),
+ JoinRule = x.State.FirstOrDefault(y => y.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.ToString(),
+ CanonicalAlias = x.State.FirstOrDefault(y => y.Type == RoomCanonicalAliasEventContent.EventId)?.RawContent?["alias"]?.ToString()
+ }).ToList(),
+ NextBatch = count > request.Limit ? rooms.Last().RoomId : null,
+ TotalRoomCountEstimate = count
+ };
+ }
+
#endregion
#region User directory
@@ -61,4 +126,29 @@ public class DirectoryController(ILogger<DirectoryController> logger, RoomStore
}
#endregion
+}
+
+public class PublicRoomDirectoryRequest {
+ [JsonPropertyName("filter")]
+ public PublicRoomDirectoryFilter Filter { get; set; }
+
+ [JsonPropertyName("include_all_networks")]
+ public bool IncludeAllNetworks { get; set; }
+
+ [JsonPropertyName("limit")]
+ public int? Limit { get; set; }
+
+ [JsonPropertyName("since")]
+ public string? Since { get; set; }
+
+ [JsonPropertyName("third_party_instance_id")]
+ public string? ThirdPartyInstanceId { get; set; }
+
+ public class PublicRoomDirectoryFilter {
+ [JsonPropertyName("generic_search_term")]
+ public string? GenericSearchTerm { get; set; }
+
+ [JsonPropertyName("room_types")]
+ public List<string>? RoomTypes { get; set; }
+ }
}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs
new file mode 100644
index 0000000..1fb3251
--- /dev/null
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs
@@ -0,0 +1,18 @@
+using LibMatrix.HomeserverEmulator.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers;
+
+[ApiController]
+[Route("/_hse/admin")]
+public class HEAdminController(ILogger<HEAdminController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase {
+ [HttpGet("users")]
+ public async Task<List<UserStore.User>> GetUsers() {
+ return userStore._users.ToList();
+ }
+
+ [HttpGet("rooms")]
+ public async Task<List<RoomStore.Room>> GetRooms() {
+ return roomStore._rooms.ToList();
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs
new file mode 100644
index 0000000..85e4ddb
--- /dev/null
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs
@@ -0,0 +1,34 @@
+using ArcaneLibs.Collections;
+using LibMatrix.HomeserverEmulator.Services;
+using LibMatrix.Responses;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.HomeserverEmulator.Controllers;
+
+[ApiController]
+[Route("/_hse/client/v1/external_profiles")]
+public class HEClientController(ILogger<HEClientController> logger, UserStore userStore, TokenService tokenService) : ControllerBase {
+ [HttpGet]
+ public async Task<ObservableDictionary<string, LoginResponse>> GetExternalProfiles() {
+ var token = tokenService.GetAccessToken(HttpContext);
+ var user = await userStore.GetUserByToken(token);
+
+ return user.AuthorizedSessions;
+ }
+
+ [HttpPut("{name}")]
+ public async Task PutExternalProfile(string name, [FromBody] LoginResponse sessionData) {
+ var token = tokenService.GetAccessToken(HttpContext);
+ var user = await userStore.GetUserByToken(token);
+
+ user.AuthorizedSessions[name] = sessionData;
+ }
+
+ [HttpDelete("{name}")]
+ public async Task DeleteExternalProfile(string name) {
+ var token = tokenService.GetAccessToken(HttpContext);
+ var user = await userStore.GetUserByToken(token);
+
+ user.AuthorizedSessions.Remove(name);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
index 9e0c17c..ce47245 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
@@ -1,18 +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.ToList();
- }
-
- [HttpGet("rooms")]
- public async Task<List<RoomStore.Room>> GetRooms() {
- return roomStore._rooms.ToList();
- }
+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.ToList();
+ }
+
+ [HttpGet("rooms")]
+ public async Task<List<RoomStore.Room>> GetRooms() {
+ return roomStore._rooms.ToList();
+ }
}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
index b6ec6bf..61195b8 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
@@ -21,6 +21,7 @@ public class RoomTimelineController(
TokenService tokenService,
UserStore userStore,
RoomStore roomStore,
+ HseConfiguration hseConfig,
HomeserverProviderService hsProvider) : ControllerBase {
[HttpPut("send/{eventType}/{txnId}")]
public async Task<EventIdResponse> SendMessage(string roomId, string eventType, string txnId, [FromBody] JsonObject content) {
@@ -34,21 +35,21 @@ public class RoomTimelineController(
Error = "Room not found"
};
- if (!room.JoinedMembers.Any(x => x.StateKey == user.UserId))
- throw new MatrixException() {
- ErrorCode = "M_FORBIDDEN",
- Error = "User is not in the room"
- };
-
var evt = new StateEvent() {
RawContent = content,
Type = eventType
}.ToStateEvent(user, room);
- room.Timeline.Add(evt);
if (evt.Type == RoomMessageEventContent.EventId && (evt.TypedContent as RoomMessageEventContent).Body.StartsWith("!hse"))
_ = Task.Run(() => HandleHseCommand(evt, room, user));
- // else
+
+ if (!room.JoinedMembers.Any(x => x.StateKey == user.UserId))
+ throw new MatrixException() {
+ ErrorCode = "M_FORBIDDEN",
+ Error = "User is not in the room"
+ };
+
+ room.Timeline.Add(evt);
return new() {
EventId = evt.EventId
@@ -256,7 +257,8 @@ public class RoomTimelineController(
room.Timeline.Add(new StateEventResponse() {
Type = RoomMessageEventContent.EventId,
TypedContent = content,
- Sender = $"@hse:{tokenService.GenerateServerName(HttpContext)}",
+ // Sender = $"@hse:{tokenService.GenerateServerName(HttpContext)}",
+ Sender = $"@hse:{hseConfig.ServerName}",
RoomId = room.RoomId,
EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100)),
OriginServerTs = DateTimeOffset.Now.ToUnixTimeMilliseconds()
@@ -299,7 +301,7 @@ public class RoomTimelineController(
InternalSendMessage(room, url + "&i=" + i);
if (i % 5000 == 0 || i == 9999) {
- Thread.Sleep(5000);
+ // Thread.Sleep(1000);
do {
InternalSendMessage(room,
@@ -320,7 +322,7 @@ public class RoomTimelineController(
var count = 1000;
for (int i = 0; i < count; i++) {
var crq = new CreateRoomRequest() {
- Name = "Test room",
+ Name = $"Test room {i}",
CreationContent = new() {
["version"] = "11"
},
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
index cfab3a6..86c9f6a 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
@@ -34,10 +34,18 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
newSyncState = syncState.Clone();
var newSyncToken = Guid.NewGuid().ToString();
+ long stallTime = 100;
do {
syncResp = IncrementalSync(user, session, syncState);
syncResp.NextBatch = newSyncToken;
- } while (!await HasDataOrStall(syncResp) && sw.ElapsedMilliseconds < timeout);
+
+ var hasData = await HasDataOrStall(syncResp);
+ if (!hasData) {
+ await Task.Delay((int)Math.Min(stallTime, Math.Max(0, (timeout ?? 10) - sw.ElapsedMilliseconds)));
+ stallTime *= 2;
+ }
+ else break;
+ } while (sw.ElapsedMilliseconds < timeout);
if (sw.ElapsedMilliseconds > timeout) {
logger.LogTrace("Sync timed out after {Elapsed}", sw.Elapsed);
@@ -49,6 +57,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
session.SyncStates[syncResp.NextBatch] = RecalculateSyncStates(newSyncState, syncResp);
logger.LogTrace("Responding to sync after {totalElapsed}", sw.Elapsed);
+ // logger.LogTrace(syncResp.ToJson(ignoreNull: true));
return syncResp;
}
@@ -135,6 +144,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
};
// step 1: check previously synced rooms
+ int updatedRooms = 0;
foreach (var (roomId, roomPosition) in syncState.RoomPositions) {
var room = roomStore.GetRoomById(roomId);
if (room == null) {
@@ -147,9 +157,11 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
var newTimelineEvents = room.Timeline.Skip(roomPosition.TimelinePosition).ToList();
var newAccountDataEvents = room.AccountData[user.UserId].Skip(roomPosition.AccountDataPosition).ToList();
if (newTimelineEvents.Count == 0 && newAccountDataEvents.Count == 0) continue;
+ if (updatedRooms++ >= 50) break; // performance cap
data.Join[room.RoomId] = new() {
State = new(newTimelineEvents.GetCalculatedState()),
- Timeline = new(newTimelineEvents, false)
+ Timeline = new(newTimelineEvents, false),
+ AccountData = new(newAccountDataEvents)
};
}
}
@@ -160,11 +172,11 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
}
// step 2: check newly joined rooms
- var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList();
+ // var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList();
var allJoinedRooms = roomStore.GetRoomsByMember(user.UserId).ToArray();
if (allJoinedRooms.Length == 0) return data;
- var rooms = Random.Shared.GetItems(allJoinedRooms, Math.Min(allJoinedRooms.Length, 50));
+ var rooms = Random.Shared.GetItems(allJoinedRooms, Math.Min(allJoinedRooms.Length, 1));
foreach (var membership in rooms) {
var membershipContent = membership.TypedContent as RoomMemberEventContent ??
throw new InvalidOperationException("Membership event content is not RoomMemberEventContent");
@@ -211,7 +223,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
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;
@@ -246,29 +258,23 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
// };
var hasData = resp is {
- AccountData: {
- Events: { Count: > 0 }
- }
+ AccountData.Events.Count: > 0
} or {
- Presence: {
- Events: { Count: > 0 }
- }
+ Presence.Events.Count: > 0
} or {
DeviceLists: {
- Changed: { Count: > 0 },
- Left: { Count: > 0 }
+ Changed.Count: > 0,
+ Left.Count: > 0
}
} or {
- ToDevice: {
- Events: { Count: > 0 }
- }
+ ToDevice.Events.Count: > 0
} or {
Rooms: {
- Invite: { Count: > 0 }
+ Invite.Count: > 0
} or {
- Join: { Count: > 0 }
+ Join.Count: > 0
} or {
- Leave: { Count: > 0 }
+ Leave.Count: > 0
}
};
@@ -278,7 +284,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe
if (!hasData) {
// logger.LogDebug($"Sync response has no data, stalling for 1000ms: {resp.ToJson(indent: false, ignoreNull: true)}");
- await Task.Delay(10);
+ // await Task.Delay(100);
}
return hasData;
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
index 9b8ce62..93e4b4f 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
@@ -28,6 +28,11 @@ public class VersionsController(ILogger<WellKnownController> logger) : Controlle
"v1.6",
"v1.7",
"v1.8",
+ "v1.9",
+ "v1.10",
+ "v1.11",
+ "v1.12",
+ "v1.13"
},
UnstableFeatures = new()
};
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs
index bae4c74..3cec712 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs
+++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs
@@ -1,31 +1,34 @@
-using System.Text.Json.Nodes;
-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;
- }
+using System.Text.Json.Nodes;
+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}"
+ ["base_url"] = $"https://{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}"
+ ["m.server"] = $"https://{Request.Host}"
+ };
+
+ logger.LogInformation("Serving server well-known: {}", obj);
+
+ return obj;
+ }
}
\ No newline at end of file
|