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; } = "12";
public RoomNameEventContent Name { get; set; } = new();
public RoomTopicEventContent Topic { get; set; } = new();
public RoomAvatarEventContent Avatar { get; set; } = new();
public RoomCanonicalAliasEventContent CanonicalAlias { get; set; } = new();
public string AliasLocalPart { get; set; } = string.Empty;
public bool IsFederatable { get; set; } = true;
public long OwnPowerLevel { get; set; } = MatrixConstants.MaxSafeJsonInteger;
public RoomJoinRulesEventContent JoinRules { get; set; } = new() {
JoinRule = RoomJoinRulesEventContent.JoinRules.Public
};
public RoomHistoryVisibilityEventContent HistoryVisibility { get; set; } = new() {
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();
///
/// State events to be sent *before* room access is configured. Keep this small!
///
public List ImportantState { get; set; } = [];
///
/// State events to be sent *after* room access is configured, but before invites are sent.
///
public List InitialState { get; set; } = [];
///
/// Users to invite, with optional reason
///
public Dictionary Invites { get; set; } = [];
///
/// Users to ban, with optional reason
///
public Dictionary Bans { get; set; } = [];
public RoomPowerLevelEventContent PowerLevels { get; set; } = new() {
EventsDefault = 0,
UsersDefault = 0,
Kick = 50,
Invite = 50,
Ban = 50,
Redact = 50,
StateDefault = 50,
NotificationsPl = new() {
Room = 50
},
Users = [],
Events = new Dictionary {
{ RoomAvatarEventContent.EventId, 50 },
{ RoomCanonicalAliasEventContent.EventId, 50 },
{ RoomEncryptionEventContent.EventId, 100 },
{ RoomHistoryVisibilityEventContent.EventId, 100 },
{ RoomGuestAccessEventContent.EventId, 100 },
{ RoomNameEventContent.EventId, 50 },
{ RoomPowerLevelEventContent.EventId, 100 },
{ RoomServerAclEventContent.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 Dictionary AdditionalCreationContent { get; set; } = new();
public List AdditionalCreators { get; set; } = new();
public virtual async Task Create(AuthenticatedHomeserverGeneric homeserver) {
var crq = new CreateRoomRequest {
PowerLevelContentOverride = new() {
EventsDefault = 1000000,
UsersDefault = 1000000,
Kick = 1000000,
Invite = 1000000,
Ban = 1000000,
Redact = 1000000,
StateDefault = 1000000,
NotificationsPl = new() {
Room = 1000000
},
Users = new() {
{ homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger }
},
Events = new Dictionary {
{ RoomAvatarEventContent.EventId, 1000000 },
{ 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
};
if (!string.IsNullOrWhiteSpace(Type))
crq.CreationContent.Add("type", Type);
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);
await SetStatesAsync(room, InitialState);
await SendInvites(room);
return room;
}
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);
}
catch (MatrixException e) {
Console.Error.WriteLine("Failed to invite {0} to {1}: {2}", kvp.Key, room.RoomId, e.Message);
}
});
await Task.WhenAll(inviteTasks);
}
private async Task SetStatesAsync(GenericRoom room, List 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) {
if (!string.IsNullOrWhiteSpace(Name.Name))
await room.SendStateEventAsync(RoomNameEventContent.EventId, Name);
if (!string.IsNullOrWhiteSpace(Topic.Topic))
await room.SendStateEventAsync(RoomTopicEventContent.EventId, Topic);
if (!string.IsNullOrWhiteSpace(Avatar.Url))
await room.SendStateEventAsync(RoomAvatarEventContent.EventId, Avatar);
if (!string.IsNullOrWhiteSpace(AliasLocalPart))
CanonicalAlias.Alias = $"#{AliasLocalPart}:{room.Homeserver.ServerName}";
if (!string.IsNullOrWhiteSpace(CanonicalAlias.Alias)) {
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) {
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))
await room.SendStateEventAsync(RoomHistoryVisibilityEventContent.EventId, HistoryVisibility);
if (!string.IsNullOrWhiteSpace(JoinRules.JoinRuleValue))
await room.SendStateEventAsync(RoomJoinRulesEventContent.EventId, JoinRules);
}
}
public class MatrixConstants {
public const long MaxSafeJsonInteger = 9007199254740991L; // 2^53 - 1
}