about summary refs log tree commit diff
path: root/LibMatrix/Helpers
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix/Helpers')
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs32
-rw-r--r--LibMatrix/Helpers/MessageFormatter.cs7
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs279
-rw-r--r--LibMatrix/Helpers/RoomUpgradeBuilder.cs232
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs27
-rw-r--r--LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs44
-rw-r--r--LibMatrix/Helpers/SyncStateResolver.cs2
7 files changed, 586 insertions, 37 deletions
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs

index 5e2b1b7..f753bf7 100644 --- a/LibMatrix/Helpers/MessageBuilder.cs +++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -37,6 +37,10 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } + public static string GetColoredBody(string color, string body) { + return $"<font color=\"{color}\">{body}</font>"; + } + public MessageBuilder WithColoredBody(string color, string body) { Content.Body += body; Content.FormattedBody += $"<font color=\"{color}\">{body}</font>"; @@ -91,9 +95,33 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } - public MessageBuilder WithMention(string id, string? displayName = null) { - Content.Body += $"@{displayName ?? id}"; + public MessageBuilder WithMention(string id, string? displayName = null, string[]? vias = null, bool useIdInPlainText = false, bool useLinkInPlainText = false) { + if (!useLinkInPlainText) Content.Body += $"@{(useIdInPlainText ? id : displayName ?? id)}"; + else { + Content.Body += $"https://matrix.to/#/{id}"; + if (vias is { Length: > 0 }) Content.Body += $"?via={string.Join("&via=", vias)}"; + } + Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; + if (id == "@room") { + Content.Mentions ??= new(); + Content.Mentions.Room = true; + } + else if (id.StartsWith('@')) { + Content.Mentions ??= new(); + Content.Mentions.Users ??= new(); + Content.Mentions.Users.Add(id); + } + + return this; + } + + public MessageBuilder WithRoomMention() { + // Legacy push rules support + Content.Body += "@room"; + Content.FormattedBody += "@room"; + Content.Mentions ??= new(); + Content.Mentions.Room = true; return this; } diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs
index 1b9b4f3..780ac0e 100644 --- a/LibMatrix/Helpers/MessageFormatter.cs +++ b/LibMatrix/Helpers/MessageFormatter.cs
@@ -30,8 +30,11 @@ public static class MessageFormatter { public static string HtmlFormatMention(string id, string? displayName = null) => $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; - public static string HtmlFormatMessageLink(string roomId, string eventId, string[]? servers = null, string? displayName = null) { - if (servers is not { Length: > 0 }) servers = new[] { roomId.Split(':', 2)[1] }; + public static string HtmlFormatMessageLink(string roomId, string eventId, string[] servers, string? displayName = null) { + if (servers is not { Length: > 0 }) + servers = roomId.Contains(':') + ? [roomId.Split(':', 2)[1]] + : throw new ArgumentException("Message links must contain a list of via's for v12+ rooms!", nameof(servers)); return $"<a href=\"https://matrix.to/#/{roomId}/{eventId}?via={string.Join("&via=", servers)}\">{displayName ?? eventId}</a>"; } diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs new file mode 100644
index 0000000..a292f33 --- /dev/null +++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -0,0 +1,279 @@ +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(); + + /// <summary> + /// State events to be sent *before* room access is configured. Keep this small! + /// </summary> + 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<MatrixEvent> InitialState { get; set; } = []; + + /// <summary> + /// Users to invite, with optional reason + /// </summary> + public Dictionary<string, string?> Invites { get; set; } = []; + + /// <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, + Invite = 50, + Ban = 50, + Redact = 50, + StateDefault = 50, + NotificationsPl = new() { + Room = 50 + }, + Users = [], + Events = new Dictionary<string, long> { + { 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<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, + Kick = 1000000, + Invite = 1000000, + Ban = 1000000, + Redact = 1000000, + StateDefault = 1000000, + NotificationsPl = new() { + Room = 1000000 + }, + Users = new() { + { homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger } + }, + Events = new Dictionary<string, long> { + { 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<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) { + 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 +} \ No newline at end of file diff --git a/LibMatrix/Helpers/RoomUpgradeBuilder.cs b/LibMatrix/Helpers/RoomUpgradeBuilder.cs new file mode 100644
index 0000000..ced0ef3 --- /dev/null +++ b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
@@ -0,0 +1,232 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.Json.Serialization; +using ArcaneLibs; +using LibMatrix.EventTypes; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using LibMatrix.StructuredData; + +namespace LibMatrix.Helpers; + +public class RoomUpgradeBuilder : RoomBuilder { + public RoomUpgradeOptions UpgradeOptions { get; set; } = new(); + public string OldRoomId { get; set; } = string.Empty; + public bool CanUpgrade { get; private set; } + public Dictionary<string, object> AdditionalTombstoneContent { get; set; } = new(); + + private List<Type> basePolicyTypes = []; + + public async Task ImportAsync(GenericRoom OldRoom) { + var sw = Stopwatch.StartNew(); + var total = 0; + + basePolicyTypes = ClassCollector<PolicyRuleEventContent>.ResolveFromAllAccessibleAssemblies().ToList(); + Console.WriteLine($"Found {basePolicyTypes.Count} policy types in {sw.ElapsedMilliseconds}ms"); + CanUpgrade = ( + (await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId) + ?? (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId) + ) + || (OldRoom.IsV12PlusRoomId && (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId)); + + await foreach (var srcEvt in OldRoom.GetFullStateAsync()) { + total++; + if (srcEvt is null) continue; + var evt = srcEvt; + + if (UpgradeOptions.UpgradeUnstableValues) { + evt = UpgradeUnstableValues(evt); + } + + if (evt.StateKey == "") { + if (evt.Type == RoomCreateEventContent.EventId) + foreach (var (key, value) in evt.RawContent) { + if (key is "room_version" or "creator") continue; + if (key == "type") + Type = value!.GetValue<string>(); + else AdditionalCreationContent[key] = value; + } + else if (evt.Type == RoomNameEventContent.EventId) + Name = evt.ContentAs<RoomNameEventContent>()!; + else if (evt.Type == RoomTopicEventContent.EventId) + Topic = evt.ContentAs<RoomTopicEventContent>()!; + else if (evt.Type == RoomAvatarEventContent.EventId) + Avatar = evt.ContentAs<RoomAvatarEventContent>()!; + else if (evt.Type == RoomCanonicalAliasEventContent.EventId) { + CanonicalAlias = evt.ContentAs<RoomCanonicalAliasEventContent>()!; + AliasLocalPart = CanonicalAlias.Alias?.Split(':', 2).FirstOrDefault()?[1..] ?? string.Empty; + } + else if (evt.Type == RoomJoinRulesEventContent.EventId) + JoinRules = evt.ContentAs<RoomJoinRulesEventContent>()!; + else if (evt.Type == RoomHistoryVisibilityEventContent.EventId) + HistoryVisibility = evt.ContentAs<RoomHistoryVisibilityEventContent>()!; + else if (evt.Type == RoomGuestAccessEventContent.EventId) + GuestAccess = evt.ContentAs<RoomGuestAccessEventContent>()!; + else if (evt.Type == RoomServerAclEventContent.EventId) + ServerAcls = evt.ContentAs<RoomServerAclEventContent>()!; + else if (evt.Type == RoomPowerLevelEventContent.EventId) { + PowerLevels = evt.ContentAs<RoomPowerLevelEventContent>()!; + if (UpgradeOptions.InvitePowerlevelUsers && PowerLevels.Users != null) + foreach (var (userId, level) in PowerLevels.Users) + if (level > PowerLevels.UsersDefault) + Invites.Add(userId, "Room upgrade (had a power level)"); + } + else if (evt.Type == RoomEncryptionEventContent.EventId) + Encryption = evt.ContentAs<RoomEncryptionEventContent>(); + else if (evt.Type == RoomPinnedEventContent.EventId) ; // Discard as you can't cross reference pinned events + else + InitialState.Add(new() { + Type = evt.Type, + StateKey = evt.StateKey, + RawContent = evt.RawContent + }); + } + else if (evt.Type == RoomMemberEventContent.EventId) { + if (evt.TypedContent is RoomMemberEventContent { Membership: "join" or "invite" } invitedMember) { + if (UpgradeOptions.InviteMembers) + Invites.TryAdd(evt.StateKey!, invitedMember.Reason ?? "Room upgrade"); + else if (UpgradeOptions.InviteLocalMembers && UserId.Parse(evt.StateKey!).ServerName == OldRoom.Homeserver.ServerName) + Invites.TryAdd(evt.StateKey!, invitedMember.Reason ?? "Room upgrade (local user)"); + } + else if (UpgradeOptions.MigrateBans && evt.TypedContent is RoomMemberEventContent { Membership: "ban" } bannedMember) + Bans.TryAdd(evt.StateKey!, bannedMember.Reason); + } + else if (!UpgradeOptions.MigrateEmptyStateEvents && evt.RawContent.Count == 0) { } // skip empty state events + else if (basePolicyTypes.Contains(evt.MappedType)) ImportPolicyEventAsync(evt); + else + InitialState.Add(new() { + Type = evt.Type, + StateKey = evt.StateKey, + RawContent = evt.RawContent + }); + } + + Console.WriteLine($"Imported {total} state events from old room {OldRoom.RoomId} in {sw.ElapsedMilliseconds}ms"); + } + + private MatrixEventResponse UpgradeUnstableValues(MatrixEventResponse evt) { + if (evt.IsLegacyType) { + var oldType = evt.Type; + evt.Type = evt.MappedType.GetCustomAttributes<MatrixEventAttribute>().FirstOrDefault(x => !x.Legacy)!.EventName; + Console.WriteLine($"Upgraded event type from {oldType} to {evt.Type} for event {evt.EventId}"); + } + + if (evt.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) { + if (evt.RawContent["recommendation"]?.GetValue<string>() == "org.matrix.mjolnir.ban") { + evt.RawContent["recommendation"] = "m.ban"; + Console.WriteLine($"Upgraded recommendation from 'org.matrix.mjolnir.ban' to 'm.ban' for event {evt.EventId}"); + } + } + + return evt; + } + + private void ImportPolicyEventAsync(MatrixEventResponse evt) { + var msc4321Options = UpgradeOptions.Msc4321PolicyListUpgradeOptions; + if (msc4321Options is { Enable: true, UpgradeType: Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition }) + return; // this upgrade type doesnt copy policies + if (msc4321Options.Enable) { + evt.RawContent["org.matrix.msc4321.original_sender"] = evt.Sender; + evt.RawContent["org.matrix.msc4321.original_timestamp"] = evt.OriginServerTs; + evt.RawContent["org.matrix.msc4321.original_event_id"] = evt.EventId; + } + + InitialState.Add(new() { + Type = evt.Type, + StateKey = evt.StateKey, + RawContent = evt.RawContent + }); + } + + public override async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) { + var oldRoom = homeserver.GetRoom(OldRoomId); + // set the previous room relation + AdditionalCreationContent["predecessor"] = new { + room_id = OldRoomId, + // event_id = (await oldRoom.GetMessagesAsync(limit: 1)).Chunk.Last().EventId + }; + + if (UpgradeOptions.NoopUpgrade) { + AliasLocalPart = null; + CanonicalAlias = new(); + return await base.Create(homeserver); + } + + // prepare old room first... + if (!string.IsNullOrWhiteSpace(AliasLocalPart)) { + var aliasResult = await homeserver.ResolveRoomAliasAsync($"#{AliasLocalPart}:{homeserver.ServerName}"); + if (aliasResult?.RoomId == OldRoomId) + await homeserver.DeleteRoomAliasAsync($"#{AliasLocalPart}:{homeserver.ServerName}"); + else + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, + Error = $"Cannot upgrade room {OldRoomId} as it has an alias that is not the same as the one tracked by the server! Server says: {aliasResult.RoomId}" + }; + } + + var room = await base.Create(homeserver); + if (CanUpgrade || UpgradeOptions.ForceUpgrade) { + if (UpgradeOptions.RoomUpgradeNotice != null) { + var noticeContent = await UpgradeOptions.RoomUpgradeNotice(room); + await oldRoom.SendMessageEventAsync(noticeContent); + } + + var tombstoneContent = new RoomTombstoneEventContent { + Body = "This room has been upgraded to a new version.", + ReplacementRoom = room.RoomId + }; + + tombstoneContent.AdditionalData ??= []; + foreach (var (key, value) in AdditionalTombstoneContent) + tombstoneContent.AdditionalData[key] = value; + + await oldRoom.SendStateEventAsync(RoomTombstoneEventContent.EventId, tombstoneContent); + } + + return room; + } + + public class RoomUpgradeOptions { + public bool InviteMembers { get; set; } + public bool InviteLocalMembers { get; set; } + public bool InvitePowerlevelUsers { get; set; } + public bool MigrateBans { get; set; } + public bool MigrateEmptyStateEvents { get; set; } + public bool UpgradeUnstableValues { get; set; } + public bool ForceUpgrade { get; set; } + public bool NoopUpgrade { get; set; } + public Msc4321PolicyListUpgradeOptions Msc4321PolicyListUpgradeOptions { get; set; } = new(); + + [JsonIgnore] + public Func<GenericRoom, Task<RoomMessageEventContent>>? RoomUpgradeNotice { get; set; } = async newRoom => new MessageBuilder() + .WithRoomMention() + .WithNewline() + .WithBody("This room has been upgraded to a new version. This version of the room will be kept as an archive.") + .WithNewline() + .WithBody("You can join the new room by clicking the link below:") + .WithNewline() + .WithMention(newRoom.RoomId, await newRoom.GetNameOrFallbackAsync(), vias: (await newRoom.GetHomeserversInRoom()).ToArray(), useLinkInPlainText: true) + .Build(); + } + + public class Msc4321PolicyListUpgradeOptions { + public bool Enable { get; set; } = true; + public Msc4321PolicyListUpgradeType UpgradeType { get; set; } = Msc4321PolicyListUpgradeType.Move; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum Msc4321PolicyListUpgradeType { + /// <summary> + /// Copy policies, unwatch old list + /// </summary> + Move, + + /// <summary> + /// Don't copy policies, watch both lists + /// </summary> + Transition + } + } +} \ No newline at end of file diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 6f2cacc..ebe653c 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null, IStorageProvider? storageProvider = null) { - private readonly Func<SyncResponse?, Task<SyncResponse?>> _msc4222EmulationSyncProcessor = new Msc4222EmulationSyncProcessor(homeserver).EmulateMsc4222; + private readonly Func<SyncResponse?, Task<SyncResponse?>> _msc4222EmulationSyncProcessor = new Msc4222EmulationSyncProcessor(homeserver, logger).EmulateMsc4222; private SyncFilter? _filter; private string? _namedFilterName; @@ -25,7 +25,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg public string? Since { get; set; } public int Timeout { get; set; } = 30000; public string? SetPresence { get; set; } - + /// <summary> /// Disabling this uses a technically slower code path, useful for checking whether delay comes from waiting for server or deserialising responses /// </summary> @@ -37,11 +37,11 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg field = value; if (value) { AsyncSyncPreprocessors.Add(_msc4222EmulationSyncProcessor); - Console.WriteLine($"Added MSC4222 emulation sync processor"); + logger?.LogInformation($"Added MSC4222 emulation sync processor"); } else { AsyncSyncPreprocessors.Remove(_msc4222EmulationSyncProcessor); - Console.WriteLine($"Removed MSC4222 emulation sync processor"); + logger?.LogInformation($"Removed MSC4222 emulation sync processor"); } } } = false; @@ -121,7 +121,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg } if (storageProvider is null) { - var res = await SyncAsyncInternal(cancellationToken, noDelay); + var res = await SyncAsyncInternal(cancellationToken, noDelay); if (res is null) return null; if (UseMsc4222StateAfter) res.Msc4222Method = SyncResponse.Msc4222SyncType.Server; @@ -186,13 +186,12 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg else { var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None); if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request"); - logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed); + var receivedTime = sw.Elapsed; var deserializeSw = Stopwatch.StartNew(); - // var jsonResp = await httpResp.Content.ReadFromJsonAsync<JsonObject>(cancellationToken: cancellationToken ?? CancellationToken.None); - // var resp = jsonResp.Deserialize<SyncResponse>(); resp = await httpResp.Content.ReadFromJsonAsync(cancellationToken: cancellationToken ?? CancellationToken.None, jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse); - logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed); + logger?.LogInformation("Deserialized sync response: {} bytes, {} response time, {} deserialize time, {} total", httpResp.GetContentLength(), receivedTime, + deserializeSw.Elapsed, sw.Elapsed); } var timeToWait = MinimumDelay.Subtract(sw.Elapsed); @@ -299,9 +298,9 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg if (syncResponse.Rooms is { Join.Count: > 0 }) foreach (var updatedRoom in syncResponse.Rooms.Join) { if (updatedRoom.Value.Timeline is null) continue; - foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events ?? []) { - stateEventResponse.RoomId = updatedRoom.Key; - var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList(); + foreach (var MatrixEventResponse in updatedRoom.Value.Timeline.Events ?? []) { + MatrixEventResponse.RoomId = updatedRoom.Key; + var tasks = TimelineEventHandlers.Select(x => x(MatrixEventResponse)).ToList(); await Task.WhenAll(tasks); } } @@ -320,12 +319,12 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg /// <summary> /// Event fired when a timeline event is received /// </summary> - public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new(); + public List<Func<MatrixEventResponse, Task>> TimelineEventHandlers { get; } = new(); /// <summary> /// Event fired when an account data event is received /// </summary> - public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); + public List<Func<MatrixEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); /// <summary> /// Event fired when an exception is thrown diff --git a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
index 6cb42ca..c887f6e 100644 --- a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs +++ b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
@@ -1,16 +1,18 @@ using System.Diagnostics; +using System.Timers; using ArcaneLibs.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; +using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers.SyncProcessors; -public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver) { - private static bool StateEventsMatch(StateEventResponse a, StateEventResponse b) { +public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver, ILogger? logger) { + private static bool StateEventsMatch(MatrixEventResponse a, MatrixEventResponse b) { return a.Type == b.Type && a.StateKey == b.StateKey; } - private static bool StateEventIsNewer(StateEventResponse a, StateEventResponse b) { + private static bool StateEventIsNewer(MatrixEventResponse a, MatrixEventResponse b) { return StateEventsMatch(a, b) && a.OriginServerTs < b.OriginServerTs; } @@ -22,12 +24,13 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese resp.Rooms.Join?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true || resp.Rooms.Leave?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true ) { - Console.WriteLine($"Msc4222EmulationSyncProcessor.EmulateMsc4222 determined that no emulation is needed in {sw.Elapsed}"); + logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug, + "Msc4222EmulationSyncProcessor.EmulateMsc4222 determined that no emulation is needed in {elapsed}", sw.Elapsed); return resp; } resp = await EmulateMsc4222Internal(resp, sw); - + return SimpleSyncProcessors.FillRoomIds(resp); } @@ -42,14 +45,17 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese tasks.AddRange(resp.Rooms.Leave.Select(ProcessLeftRooms).ToList()); } - var tasksEnum = tasks.ToAsyncEnumerable(); + var tasksEnum = tasks.ToAsyncResultEnumerable(); await foreach (var wasModified in tasksEnum) { if (wasModified) { modified = true; } } - Console.WriteLine($"Msc4222EmulationSyncProcessor.EmulateMsc4222 processed {resp.Rooms?.Join?.Count}/{resp.Rooms?.Leave?.Count} rooms in {sw.Elapsed} (modified: {modified})"); + logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug, + "Msc4222EmulationSyncProcessor.EmulateMsc4222 processed {joinCount}/{leaveCount} rooms in {elapsed} (modified: {modified})", + resp.Rooms?.Join?.Count ?? 0, resp.Rooms?.Leave?.Count ?? 0, sw.Elapsed, modified); + if (modified) resp.Msc4222Method = SyncResponse.Msc4222SyncType.Emulated; @@ -70,7 +76,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese Events = [] }; - var oldState = new List<StateEventResponse>(); + var oldState = new List<MatrixEventResponse>(); if (data.State is { Events.Count: > 0 }) { oldState.ReplaceBy(data.State.Events, StateEventIsNewer); } @@ -90,7 +96,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese } } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e); } } @@ -103,12 +109,12 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese // .Join(oldState, x => (x.Type, x.StateKey), y => (y.Type, y.StateKey), (x, y) => x) .IntersectBy(oldState.Select(s => (s.Type, s.StateKey)), s => (s.Type, s.StateKey)) .ToList(); - + data.State = null; return true; } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e); } var tasks = oldState @@ -117,12 +123,13 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!); } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get state event {oldEvt.Type}/{oldEvt.StateKey} for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}", + oldEvt.Type, oldEvt.StateKey, roomId, e); return oldEvt; } }); - var tasksEnum = tasks.ToAsyncEnumerable(); + var tasksEnum = tasks.ToAsyncResultEnumerable(); await foreach (var evt in tasksEnum) { data.StateAfter.Events.Add(evt); } @@ -150,10 +157,10 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese return true; } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e); } - var oldState = new List<StateEventResponse>(); + var oldState = new List<MatrixEventResponse>(); if (data.State is { Events.Count: > 0 }) { oldState.ReplaceBy(data.State.Events, StateEventIsNewer); } @@ -173,7 +180,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese } } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e); } } @@ -185,12 +192,13 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!); } catch (Exception e) { - Console.WriteLine($"Msc4222Emulation: Failed to get state event {oldEvt.Type}/{oldEvt.StateKey} for room {roomId}, state may be incomplete!\n{e}"); + logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}", + oldEvt.Type, oldEvt.StateKey, roomId, e); return oldEvt; } }); - var tasksEnum = tasks.ToAsyncEnumerable(); + var tasksEnum = tasks.ToAsyncResultEnumerable(); await foreach (var evt in tasksEnum) { data.StateAfter.Events.Add(evt); } diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs
index f111c79..17c1a41 100644 --- a/LibMatrix/Helpers/SyncStateResolver.cs +++ b/LibMatrix/Helpers/SyncStateResolver.cs
@@ -625,7 +625,7 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge return oldState; } - private static EventList? MergeEventListBy(EventList? oldState, EventList? newState, Func<StateEventResponse, StateEventResponse, bool> comparer) { + private static EventList? MergeEventListBy(EventList? oldState, EventList? newState, Func<MatrixEventResponse, MatrixEventResponse, bool> comparer) { if (newState is null) return oldState; if (oldState is null) { return newState;