/
blueprint.go
335 lines (320 loc) · 9.45 KB
/
blueprint.go
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package internal
import (
"encoding/json"
"log"
"regexp"
"github.com/matrix-org/complement/b"
"github.com/tidwall/gjson"
)
var ignoredEventType = map[string]bool{
"m.room.tombstone": true, // TODO: need to hit /upgrade API
"m.room.encrypted": true, // TODO: need to be able to send E2E messages and then give keys to homerunner clients
"m.reaction": true, // TODO: uhh not in spec, needs event_id mapping
"m.room.redaction": true, // TODO: Hit /redact API, needs event_id mapping first
}
var regexpAlphanums = regexp.MustCompile("[^a-zA-Z0-9]+")
// ConvertToBlueprint converts a /sync snapshot to a Complement blueprint
func ConvertToBlueprint(s *Snapshot, serverName string) (*b.Blueprint, error) {
bp := &b.Blueprint{
// format that Docker images are happy with
Name: "snapshot_" + regexpAlphanums.ReplaceAllString(s.UserID, ""),
// only keep the access token for the user whose account is being snapshotted else
// we could persist 10,000s of tokens as labels and make Docker sad
KeepAccessTokensForUsers: []string{s.UserID},
}
// TODO: the snapshot has information on servers but we only want 1 server for now
hs := b.Homeserver{
Name: serverName,
}
log.Printf("Blueprint: creating users (%d)\n", len(s.Devices))
// Create all the users (and devices) in the snapshot
for userID, devices := range s.Devices {
local, _ := split(userID)
for i := range devices {
user := b.User{
Localpart: local,
DisplayName: local,
DeviceID: &devices[i],
}
// set up OTKs if this user+device sends E2E messages
if devices[i] != NoEncryptedDevice {
user.OneTimeKeys = 100
}
// set DM list if this is the syncing user
if userID == s.UserID {
user.AccountData = append(user.AccountData, b.AccountData{
Type: "m.direct",
Value: map[string]interface{}{
"content": s.AccountDataDMs,
},
})
}
hs.Users = append(hs.Users, user)
}
}
log.Printf("Blueprint: creating rooms (%d)\n", len(s.Rooms))
for i := range s.Rooms {
room := convertRoom(&s.Rooms[i])
if room == nil {
log.Printf(" skipping room %v\n", s.Rooms[i].ID)
continue
}
hs.Rooms = append(hs.Rooms, *room)
}
// remove users who have left and done nothing else of consequence
removeUnusedUsers(&hs)
log.Printf("Blueprint: cleaned user list (%d)\n", len(hs.Users))
bp.Homeservers = append(bp.Homeservers, hs)
return bp, nil
}
func convertRoom(sr *AnonSnapshotRoom) *b.Room {
if len(sr.State) == 0 {
return convertTimelineOnlyRoom(sr)
}
r := &b.Room{
Ref: sr.ID,
Creator: sr.Creator,
}
// find and set the create content and memberships
memberships := map[string][]string{}
otherState := []json.RawMessage{}
var creatorMembership string
var plEvent json.RawMessage
for _, ev := range sr.State {
evType := gjson.GetBytes(ev, "type").Str
if ignoredEventType[evType] {
continue
}
switch evType {
case "m.room.create":
var createContent map[string]interface{}
if err := json.Unmarshal([]byte(gjson.GetBytes(ev, "content").Raw), &createContent); err != nil {
log.Printf(" cannot convert room, cannot unmarshal m.room.create content: %s\n", err)
return nil
}
r.CreateRoom = createContent
case "m.room.member":
membership := gjson.GetBytes(ev, "content.membership").Str
userID := gjson.GetBytes(ev, "state_key").Str
if userID == sr.Creator {
// we handle the creator separately as they are the magic user who sets room pre-state
creatorMembership = membership
} else {
memberships[membership] = append(memberships[membership], userID)
}
case "m.room.power_levels":
plEvent = ev
default:
otherState = append(otherState, ev)
}
}
// Make the room publicly joinable then join all the users in the room
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
Type: "m.room.join_rules",
StateKey: b.Ptr(""),
Content: map[string]interface{}{
"join_rule": "public",
},
})
// All joined users should be joined
for _, userID := range memberships["join"] {
r.Events = append(r.Events, b.Event{
Sender: userID,
StateKey: b.Ptr(userID),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "join",
},
})
}
/*
// All left users should join then immediately leave
for _, userID := range memberships["leave"] {
r.Events = append(r.Events, b.Event{
Sender: userID,
StateKey: b.Ptr(userID),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "join",
},
})
r.Events = append(r.Events, b.Event{
Sender: userID,
StateKey: b.Ptr(userID),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "leave",
},
})
} */
// All invited users should be invited, we'll just invite them from the creator as they will be joined with perms
for _, userID := range memberships["invite"] {
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
StateKey: b.Ptr(userID),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "invite",
},
})
}
// All banned users should be banned, we'll ban them from the creator as they will be joined with perms
for _, userID := range memberships["ban"] {
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
StateKey: b.Ptr(userID),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "ban",
},
})
}
// Set all the /sync State (excluding create/member events) from the creator as they will be joined with perms
// the PL event comes last as it may make the creator unable to do stuff
for _, ev := range otherState {
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
StateKey: b.Ptr(gjson.GetBytes(ev, "state_key").Str),
Type: gjson.GetBytes(ev, "type").Str,
Content: jsonObject([]byte(gjson.GetBytes(ev, "content").Raw)),
})
}
if plEvent != nil {
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
StateKey: b.Ptr(""),
Type: "m.room.power_levels",
Content: jsonObject([]byte(gjson.GetBytes(plEvent, "content").Raw)),
})
}
// roll forward Timeline
for _, ev := range sr.Timeline {
evType := gjson.GetBytes(ev, "type").Str
if ignoredEventType[evType] {
continue
}
var sk *string
skg := gjson.GetBytes(ev, "state_key")
if skg.Exists() {
sk = &skg.Str
}
if evType == "m.room.member" && sk != nil {
membership := gjson.GetBytes(ev, "content.membership").Str
if *sk == sr.Creator {
// merge multiple creator memberships into one so we never end up leaving the room completely empty
// which can happen if everyone leaves the room in State but then joins again in Timeline
creatorMembership = membership
continue
} else if membership == "leave" {
// we can't do leave -> leave transitions else we get "User \"@anon-4c9:hs1\" is not a member of room"
// so they must either be invite/join/ban
canBeLeft := false
for _, uid := range memberships["invite"] {
if uid == *sk {
canBeLeft = true
break
}
}
for _, uid := range memberships["join"] {
if uid == *sk {
canBeLeft = true
break
}
}
for _, uid := range memberships["ban"] {
if uid == *sk {
canBeLeft = true
break
}
}
if !canBeLeft {
continue
}
}
}
r.Events = append(r.Events, b.Event{
Sender: gjson.GetBytes(ev, "sender").Str,
StateKey: sk,
Type: evType,
Content: jsonObject([]byte(gjson.GetBytes(ev, "content").Raw)),
})
}
// NOW handle the creator's membership
// We need to inspect the PL event to accurately handle invite/ban
switch creatorMembership {
case "invite": // TODO: handle this properly
fallthrough
case "ban": // TODO: handle this properly
fallthrough
case "leave":
r.Events = append(r.Events, b.Event{
Sender: sr.Creator,
StateKey: b.Ptr(sr.Creator),
Type: "m.room.member",
Content: map[string]interface{}{
"membership": "leave",
},
})
}
return r
}
func convertTimelineOnlyRoom(sr *AnonSnapshotRoom) *b.Room {
r := &b.Room{
Ref: sr.ID,
Creator: sr.Creator,
}
for _, ev := range sr.Timeline {
evType := gjson.GetBytes(ev, "type").Str
if ignoredEventType[evType] {
continue
}
switch evType {
case "m.room.create":
var createContent map[string]interface{}
if err := json.Unmarshal([]byte(gjson.GetBytes(ev, "content").Raw), &createContent); err != nil {
log.Printf(" cannot convert room, cannot unmarshal m.room.create content: %s\n", err)
return nil
}
r.CreateRoom = createContent
default:
var sk *string
skg := gjson.GetBytes(ev, "state_key")
if skg.Exists() {
sk = &skg.Str
}
r.Events = append(r.Events, b.Event{
Sender: gjson.GetBytes(ev, "sender").Str,
StateKey: sk,
Type: evType,
Content: jsonObject([]byte(gjson.GetBytes(ev, "content").Raw)),
})
}
}
return r
}
func removeUnusedUsers(hs *b.Homeserver) {
usersWhoDoSomething := make(map[string]bool)
for _, room := range hs.Rooms {
for _, ev := range room.Events {
if ev.Type == "m.room.member" && ev.StateKey != nil {
localpartStateKey, _ := split(*ev.StateKey)
usersWhoDoSomething[localpartStateKey] = true
}
localpart, _ := split(ev.Sender)
usersWhoDoSomething[localpart] = true
}
}
var users []b.User
for i := 0; i < len(hs.Users); i++ {
if !usersWhoDoSomething[hs.Users[i].Localpart] {
continue
}
users = append(users, hs.Users[i])
}
hs.Users = users
}
func jsonObject(in json.RawMessage) (out map[string]interface{}) {
_ = json.Unmarshal(in, &out)
return
}