diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index 6f55739..f753bf7 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -95,8 +95,13 @@ 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();
@@ -111,6 +116,15 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
return this;
}
+ public MessageBuilder WithRoomMention() {
+ // Legacy push rules support
+ Content.Body += "@room";
+ Content.FormattedBody += "@room";
+ Content.Mentions ??= new();
+ Content.Mentions.Room = true;
+ return this;
+ }
+
public MessageBuilder WithNewline() {
Content.Body += "\n";
Content.FormattedBody += "<br>";
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
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))
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 c8e2928..ebe653c 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -298,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);
}
}
@@ -319,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 e34b5cf..c887f6e 100644
--- a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
+++ b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
@@ -8,11 +8,11 @@ using Microsoft.Extensions.Logging;
namespace LibMatrix.Helpers.SyncProcessors;
public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver, ILogger? logger) {
- private static bool StateEventsMatch(StateEventResponse a, StateEventResponse b) {
+ 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;
}
@@ -45,7 +45,7 @@ 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;
@@ -76,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);
}
@@ -129,7 +129,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
}
});
- var tasksEnum = tasks.ToAsyncEnumerable();
+ var tasksEnum = tasks.ToAsyncResultEnumerable();
await foreach (var evt in tasksEnum) {
data.StateAfter.Events.Add(evt);
}
@@ -160,7 +160,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
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);
}
@@ -198,7 +198,7 @@ public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homese
}
});
- 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;
|