about summary refs log tree commit diff
path: root/LibMatrix/Helpers/RoomUpgradeBuilder.cs
blob: 85a5e36905dd81439e30d68b5efa617a2cc970a1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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 StateEventResponse UpgradeUnstableValues(StateEventResponse 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(StateEventResponse 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
        }
    }
}