diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index bef7568..a292f33 100644
--- a/LibMatrix/Helpers/RoomBuilder.cs
+++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -1,14 +1,20 @@
+using System.Diagnostics;
using System.Runtime.Intrinsics.X86;
+using System.Text.RegularExpressions;
+using ArcaneLibs.Extensions;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
using LibMatrix.RoomTypes;
+using LibMatrix.StructuredData;
namespace LibMatrix.Helpers;
public class RoomBuilder {
+ private static readonly string[] V12PlusRoomVersions = ["org.matrix.hydra.11", "12"];
+ public bool SynapseAdminAutoAcceptLocalInvites { get; set; }
public string? Type { get; set; }
- public string Version { get; set; } = "11";
+ public string Version { get; set; } = "12";
public RoomNameEventContent Name { get; set; } = new();
public RoomTopicEventContent Topic { get; set; } = new();
public RoomAvatarEventContent Avatar { get; set; } = new();
@@ -25,22 +31,37 @@ public class RoomBuilder {
HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared
};
+ public RoomGuestAccessEventContent GuestAccess { get; set; } = new() {
+ GuestAccess = "forbidden"
+ };
+
+ public RoomServerAclEventContent ServerAcls { get; set; } = new() {
+ AllowIpLiterals = false
+ };
+
+ public RoomEncryptionEventContent Encryption { get; set; } = new();
+
/// <summary>
/// State events to be sent *before* room access is configured. Keep this small!
/// </summary>
- public List<StateEvent> ImportantState { get; set; } = [];
+ public List<MatrixEvent> ImportantState { get; set; } = [];
/// <summary>
/// State events to be sent *after* room access is configured, but before invites are sent.
/// </summary>
- public List<StateEvent> InitialState { get; set; } = [];
+ public List<MatrixEvent> InitialState { get; set; } = [];
/// <summary>
/// Users to invite, with optional reason
/// </summary>
- public Dictionary<string, string?> Invites { get; set; } = new();
+ public Dictionary<string, string?> Invites { get; set; } = [];
- public RoomPowerLevelEventContent PowerLevels { get; init; } = new() {
+ /// <summary>
+ /// Users to ban, with optional reason
+ /// </summary>
+ public Dictionary<string, string?> Bans { get; set; } = [];
+
+ public RoomPowerLevelEventContent PowerLevels { get; set; } = new() {
EventsDefault = 0,
UsersDefault = 0,
Kick = 50,
@@ -57,16 +78,28 @@ public class RoomBuilder {
{ RoomCanonicalAliasEventContent.EventId, 50 },
{ RoomEncryptionEventContent.EventId, 100 },
{ RoomHistoryVisibilityEventContent.EventId, 100 },
+ { RoomGuestAccessEventContent.EventId, 100 },
{ RoomNameEventContent.EventId, 50 },
{ RoomPowerLevelEventContent.EventId, 100 },
{ RoomServerAclEventContent.EventId, 100 },
- { RoomTombstoneEventContent.EventId, 100 },
- { RoomPolicyServerEventContent.EventId, 100 }
+ { RoomTombstoneEventContent.EventId, 150 },
+ { RoomPolicyServerEventContent.EventId, 100 },
+ { RoomPinnedEventContent.EventId, 50 },
+ // recommended extensions
+ { "im.vector.modular.widgets", 50 },
+ // { "m.reaction", 0 }, // we probably don't want these to end up as room state
+ // - prevent calls
+ { "io.element.voice_broadcast_info", 50 },
+ { "org.matrix.msc3401.call", 50 },
+ { "org.matrix.msc3401.call.member", 50 },
}
};
- public async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) {
- var crq = new CreateRoomRequest() {
+ public Dictionary<string, object> AdditionalCreationContent { get; set; } = new();
+ public List<string> AdditionalCreators { get; set; } = new();
+
+ public virtual async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) {
+ var crq = new CreateRoomRequest {
PowerLevelContentOverride = new() {
EventsDefault = 1000000,
UsersDefault = 1000000,
@@ -78,7 +111,7 @@ public class RoomBuilder {
NotificationsPl = new() {
Room = 1000000
},
- Users = new Dictionary<string, long>() {
+ Users = new() {
{ homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger }
},
Events = new Dictionary<string, long> {
@@ -86,12 +119,13 @@ public class RoomBuilder {
{ RoomCanonicalAliasEventContent.EventId, 1000000 },
{ RoomEncryptionEventContent.EventId, 1000000 },
{ RoomHistoryVisibilityEventContent.EventId, 1000000 },
+ { RoomGuestAccessEventContent.EventId, 1000000 },
{ RoomNameEventContent.EventId, 1000000 },
{ RoomPowerLevelEventContent.EventId, 1000000 },
{ RoomServerAclEventContent.EventId, 1000000 },
{ RoomTombstoneEventContent.EventId, 1000000 },
{ RoomPolicyServerEventContent.EventId, 1000000 }
- }
+ },
},
Visibility = "private",
RoomVersion = Version
@@ -103,8 +137,25 @@ public class RoomBuilder {
if (!IsFederatable)
crq.CreationContent.Add("m.federate", false);
+ AdditionalCreators.RemoveAll(string.IsNullOrWhiteSpace);
+ if (V12PlusRoomVersions.Contains(Version)) {
+ crq.PowerLevelContentOverride.Users.Remove(homeserver.WhoAmI.UserId);
+ PowerLevels.Users?.Remove(homeserver.WhoAmI.UserId);
+ if (AdditionalCreators is { Count: > 0 }) {
+ crq.CreationContent.Add("additional_creators", AdditionalCreators);
+ foreach (var user in AdditionalCreators)
+ PowerLevels.Users?.Remove(user);
+ }
+ }
+
+ foreach (var kvp in AdditionalCreationContent) {
+ crq.CreationContent.Add(kvp.Key, kvp.Value);
+ }
+
var room = await homeserver.CreateRoom(crq);
+ Console.WriteLine("Press any key to continue...");
+ Console.ReadKey(true);
await SetBasicRoomInfoAsync(room);
await SetStatesAsync(room, ImportantState);
await SetAccessAsync(room);
@@ -117,6 +168,23 @@ public class RoomBuilder {
private async Task SendInvites(GenericRoom room) {
if (Invites.Count == 0) return;
+ if (SynapseAdminAutoAcceptLocalInvites && room.Homeserver is AuthenticatedHomeserverSynapse synapse) {
+ var localJoinTasks = Invites.Where(u => UserId.Parse(u.Key).ServerName == synapse.ServerName).Select(async entry => {
+ var user = entry.Key;
+ var reason = entry.Value;
+ try {
+ var uhs = await synapse.Admin.GetHomeserverForUserAsync(user, TimeSpan.FromHours(1));
+ var userRoom = uhs.GetRoom(room.RoomId);
+ await userRoom.JoinAsync([uhs.ServerName], reason);
+ await uhs.Logout();
+ }
+ catch (MatrixException e) {
+ Console.WriteLine("Failed to auto-accept invite for {0} in {1}: {2}", user, room.RoomId, e.Message);
+ }
+ }).ToList();
+ await Task.WhenAll(localJoinTasks);
+ }
+
var inviteTasks = Invites.Select(async kvp => {
try {
await room.InviteUserAsync(kvp.Key, kvp.Value);
@@ -129,12 +197,39 @@ public class RoomBuilder {
await Task.WhenAll(inviteTasks);
}
- private async Task SetStatesAsync(GenericRoom room, List<StateEvent> state) {
- foreach (var ev in state) {
- await (string.IsNullOrWhiteSpace(ev.StateKey)
- ? room.SendStateEventAsync(ev.Type, ev.RawContent)
- : room.SendStateEventAsync(ev.Type, ev.StateKey, ev.RawContent));
- }
+ private async Task SetStatesAsync(GenericRoom room, List<MatrixEvent> state) {
+ if (state.Count == 0) return;
+ await room.BulkSendEventsAsync(state);
+ // We chunk this up to try to avoid hitting reverse proxy timeouts
+ // foreach (var group in state.Chunk(chunkSize)) {
+ // var sw = Stopwatch.StartNew();
+ // await room.BulkSendEventsAsync(group);
+ // if (sw.ElapsedMilliseconds > 5000) {
+ // chunkSize = Math.Max(chunkSize / 2, 1);
+ // Console.WriteLine($"Warning: Sending {group.Length} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}.");
+ // }
+ // }
+ // int chunkSize = 50;
+ // for (int i = 0; i < state.Count; i += chunkSize) {
+ // var chunk = state.Skip(i).Take(chunkSize).ToList();
+ // if (chunk.Count == 0) continue;
+ //
+ // var sw = Stopwatch.StartNew();
+ // await room.BulkSendEventsAsync(chunk, forceSyncInterval: chunk.Count + 1);
+ // Console.WriteLine($"Sent {chunk.Count} state events in {sw.ElapsedMilliseconds}ms. {state.Count - (i + chunk.Count)} remaining.");
+ // // if (sw.ElapsedMilliseconds > 45000) {
+ // // chunkSize = Math.Max(chunkSize / 3, 1);
+ // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is dangerously long. Reducing chunk size to {chunkSize}.");
+ // // }
+ // // else if (sw.ElapsedMilliseconds > 30000) {
+ // // chunkSize = Math.Max(chunkSize / 2, 1);
+ // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}.");
+ // // }
+ // // else if (sw.ElapsedMilliseconds < 10000) {
+ // // chunkSize = Math.Min((int)(chunkSize * 1.2), 1000);
+ // // Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}.");
+ // // }
+ // }
}
private async Task SetBasicRoomInfoAsync(GenericRoom room) {
@@ -154,10 +249,21 @@ public class RoomBuilder {
await room.Homeserver.SetRoomAliasAsync(CanonicalAlias.Alias!, room.RoomId);
await room.SendStateEventAsync(RoomCanonicalAliasEventContent.EventId, CanonicalAlias);
}
+
+ if (!string.IsNullOrWhiteSpace(Encryption.Algorithm))
+ await room.SendStateEventAsync(RoomEncryptionEventContent.EventId, Encryption);
}
private async Task SetAccessAsync(GenericRoom room) {
- PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel;
+ if (!V12PlusRoomVersions.Contains(Version))
+ PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel;
+ else {
+ PowerLevels.Users!.Remove(room.Homeserver.WhoAmI.UserId);
+ foreach (var additionalCreator in AdditionalCreators) {
+ PowerLevels.Users!.Remove(additionalCreator);
+ }
+ }
+
await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, PowerLevels);
if (!string.IsNullOrWhiteSpace(HistoryVisibility.HistoryVisibility))
|