1 package org
.asamk
.signal
.manager
.storage
.recipients
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.Profile
;
5 import org
.asamk
.signal
.manager
.storage
.Utils
;
6 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
7 import org
.signal
.libsignal
.zkgroup
.profiles
.ExpiringProfileKeyCredential
;
8 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
9 import org
.slf4j
.Logger
;
10 import org
.slf4j
.LoggerFactory
;
11 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
14 import java
.io
.FileInputStream
;
15 import java
.io
.FileNotFoundException
;
16 import java
.io
.IOException
;
17 import java
.nio
.file
.Files
;
18 import java
.util
.Base64
;
19 import java
.util
.List
;
20 import java
.util
.Objects
;
21 import java
.util
.Optional
;
23 import java
.util
.stream
.Collectors
;
25 public class LegacyRecipientStore2
{
27 private static final Logger logger
= LoggerFactory
.getLogger(LegacyRecipientStore2
.class);
29 public static void migrate(File file
, RecipientStore recipientStore
) {
30 final var objectMapper
= Utils
.createStorageObjectMapper();
31 try (var inputStream
= new FileInputStream(file
)) {
32 final var storage
= objectMapper
.readValue(inputStream
, Storage
.class);
34 final var recipients
= storage
.recipients
.stream().map(r
-> {
35 final var recipientId
= new RecipientId(r
.id
, recipientStore
);
36 final var address
= new RecipientAddress(Optional
.ofNullable(r
.uuid
).map(ServiceId
::parseOrThrow
),
37 Optional
.ofNullable(r
.number
));
39 Contact contact
= null;
40 if (r
.contact
!= null) {
41 contact
= new Contact(r
.contact
.name
,
45 r
.contact
.messageExpirationTime
,
50 r
.contact
.profileSharingEnabled
,
55 ProfileKey profileKey
= null;
56 if (r
.profileKey
!= null) {
58 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
59 } catch (InvalidInputException ignored
) {
63 ExpiringProfileKeyCredential expiringProfileKeyCredential
= null;
64 if (r
.expiringProfileKeyCredential
!= null) {
66 expiringProfileKeyCredential
= new ExpiringProfileKeyCredential(Base64
.getDecoder()
67 .decode(r
.expiringProfileKeyCredential
));
68 } catch (Throwable ignored
) {
72 Profile profile
= null;
73 if (r
.profile
!= null) {
74 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
79 r
.profile
.avatarUrlPath
,
80 r
.profile
.mobileCoinAddress
== null
82 : Base64
.getDecoder().decode(r
.profile
.mobileCoinAddress
),
83 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
84 r
.profile
.capabilities
.stream()
85 .map(Profile
.Capability
::valueOfOrNull
)
86 .filter(Objects
::nonNull
)
87 .collect(Collectors
.toSet()));
90 return new Recipient(recipientId
,
94 expiringProfileKeyCredential
,
97 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
99 recipientStore
.addLegacyRecipients(recipients
);
100 } catch (FileNotFoundException e
) {
101 // nothing to migrate
102 } catch (IOException e
) {
103 logger
.warn("Failed to load recipient store", e
);
104 throw new RuntimeException(e
);
107 Files
.delete(file
.toPath());
108 } catch (IOException e
) {
109 logger
.warn("Failed to load recipient store", e
);
110 throw new RuntimeException(e
);
114 public record Storage(List
<Recipient
> recipients
, long lastId
) {
116 public record Recipient(
121 String expiringProfileKeyCredential
,
126 public record Contact(
129 int messageExpirationTime
,
132 boolean profileSharingEnabled
135 public record Profile(
136 long lastUpdateTimestamp
,
141 String avatarUrlPath
,
142 String mobileCoinAddress
,
143 String unidentifiedAccessMode
,
144 Set
<String
> capabilities