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
,
48 r
.contact
.messageExpirationTime
,
53 r
.contact
.profileSharingEnabled
,
58 ProfileKey profileKey
= null;
59 if (r
.profileKey
!= null) {
61 profileKey
= new ProfileKey(Base64
.getDecoder().decode(r
.profileKey
));
62 } catch (InvalidInputException ignored
) {
66 ExpiringProfileKeyCredential expiringProfileKeyCredential
= null;
67 if (r
.expiringProfileKeyCredential
!= null) {
69 expiringProfileKeyCredential
= new ExpiringProfileKeyCredential(Base64
.getDecoder()
70 .decode(r
.expiringProfileKeyCredential
));
71 } catch (Throwable ignored
) {
75 Profile profile
= null;
76 if (r
.profile
!= null) {
77 profile
= new Profile(r
.profile
.lastUpdateTimestamp
,
82 r
.profile
.avatarUrlPath
,
83 r
.profile
.mobileCoinAddress
== null
85 : Base64
.getDecoder().decode(r
.profile
.mobileCoinAddress
),
86 Profile
.UnidentifiedAccessMode
.valueOfOrUnknown(r
.profile
.unidentifiedAccessMode
),
87 r
.profile
.capabilities
.stream()
88 .map(Profile
.Capability
::valueOfOrNull
)
89 .filter(Objects
::nonNull
)
90 .collect(Collectors
.toSet()),
94 return new Recipient(recipientId
,
98 expiringProfileKeyCredential
,
102 }).collect(Collectors
.toMap(Recipient
::getRecipientId
, r
-> r
));
104 recipientStore
.addLegacyRecipients(recipients
);
105 } catch (FileNotFoundException e
) {
106 // nothing to migrate
107 } catch (IOException e
) {
108 logger
.warn("Failed to load recipient store", e
);
109 throw new RuntimeException(e
);
112 Files
.delete(file
.toPath());
113 } catch (IOException e
) {
114 logger
.warn("Failed to load recipient store", e
);
115 throw new RuntimeException(e
);
119 public record Storage(List
<Recipient
> recipients
, long lastId
) {
121 public record Recipient(
126 String expiringProfileKeyCredential
,
131 public record Contact(
134 int messageExpirationTime
,
137 boolean profileSharingEnabled
140 public record Profile(
141 long lastUpdateTimestamp
,
146 String avatarUrlPath
,
147 String mobileCoinAddress
,
148 String unidentifiedAccessMode
,
149 Set
<String
> capabilities