1 package org
.asamk
.signal
.manager
.storage
.groups
;
3 import org
.asamk
.signal
.manager
.api
.GroupId
;
4 import org
.asamk
.signal
.manager
.api
.GroupIdV1
;
5 import org
.asamk
.signal
.manager
.api
.GroupIdV2
;
6 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
7 import org
.asamk
.signal
.manager
.storage
.Database
;
8 import org
.asamk
.signal
.manager
.storage
.Utils
;
9 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
10 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientIdCreator
;
11 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
12 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
13 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
14 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
15 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
16 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
17 import org
.slf4j
.Logger
;
18 import org
.slf4j
.LoggerFactory
;
19 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
20 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
21 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
23 import java
.io
.IOException
;
24 import java
.sql
.Connection
;
25 import java
.sql
.ResultSet
;
26 import java
.sql
.SQLException
;
27 import java
.sql
.Types
;
28 import java
.util
.ArrayList
;
29 import java
.util
.Arrays
;
30 import java
.util
.Collection
;
31 import java
.util
.List
;
33 import java
.util
.Objects
;
35 import java
.util
.stream
.Collectors
;
36 import java
.util
.stream
.Stream
;
38 public class GroupStore
{
40 private static final Logger logger
= LoggerFactory
.getLogger(GroupStore
.class);
41 private static final String TABLE_GROUP_V2
= "group_v2";
42 private static final String TABLE_GROUP_V1
= "group_v1";
43 private static final String TABLE_GROUP_V1_MEMBER
= "group_v1_member";
45 private final Database database
;
46 private final RecipientResolver recipientResolver
;
47 private final RecipientIdCreator recipientIdCreator
;
49 public static void createSql(Connection connection
) throws SQLException
{
50 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
51 try (final var statement
= connection
.createStatement()) {
52 statement
.executeUpdate("""
53 CREATE TABLE group_v2 (
54 _id INTEGER PRIMARY KEY,
55 storage_id BLOB UNIQUE,
57 group_id BLOB UNIQUE NOT NULL,
58 master_key BLOB NOT NULL,
60 distribution_id BLOB UNIQUE NOT NULL,
61 blocked INTEGER NOT NULL DEFAULT FALSE,
62 permission_denied INTEGER NOT NULL DEFAULT FALSE
64 CREATE TABLE group_v1 (
65 _id INTEGER PRIMARY KEY,
66 storage_id BLOB UNIQUE,
68 group_id BLOB UNIQUE NOT NULL,
69 group_id_v2 BLOB UNIQUE,
72 expiration_time INTEGER NOT NULL DEFAULT 0,
73 blocked INTEGER NOT NULL DEFAULT FALSE,
74 archived INTEGER NOT NULL DEFAULT FALSE
76 CREATE TABLE group_v1_member (
77 _id INTEGER PRIMARY KEY,
78 group_id INTEGER NOT NULL REFERENCES group_v1 (_id) ON DELETE CASCADE,
79 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
80 UNIQUE(group_id, recipient_id)
87 final Database database
,
88 final RecipientResolver recipientResolver
,
89 final RecipientIdCreator recipientIdCreator
91 this.database
= database
;
92 this.recipientResolver
= recipientResolver
;
93 this.recipientIdCreator
= recipientIdCreator
;
96 public void updateGroup(GroupInfo group
) {
97 try (final var connection
= database
.getConnection()) {
98 connection
.setAutoCommit(false);
99 updateGroup(connection
, group
);
101 } catch (SQLException e
) {
102 throw new RuntimeException("Failed update recipient store", e
);
106 public void updateGroup(final Connection connection
, final GroupInfo group
) throws SQLException
{
107 final Long internalId
;
114 ).formatted(group
instanceof GroupInfoV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
115 try (final var statement
= connection
.prepareStatement(sql
)) {
116 statement
.setBytes(1, group
.getGroupId().serialize());
117 internalId
= Utils
.executeQueryForOptional(statement
, res
-> res
.getLong("_id")).orElse(null);
119 insertOrReplaceGroup(connection
, internalId
, group
);
122 public void storeStorageRecord(
123 final Connection connection
, final GroupId groupId
, final StorageId storageId
, final byte[] storageRecord
124 ) throws SQLException
{
128 SET storage_id = ?, storage_record = ?
131 ).formatted(groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
132 try (final var statement
= connection
.prepareStatement(sql
)) {
133 statement
.setBytes(1, storageId
.getRaw());
134 if (storageRecord
== null) {
135 statement
.setNull(2, Types
.BLOB
);
137 statement
.setBytes(2, storageRecord
);
139 statement
.setBytes(3, groupId
.serialize());
140 statement
.executeUpdate();
144 public void deleteGroup(GroupId groupId
) {
145 if (groupId
instanceof GroupIdV1 groupIdV1
) {
146 deleteGroup(groupIdV1
);
147 } else if (groupId
instanceof GroupIdV2 groupIdV2
) {
148 deleteGroup(groupIdV2
);
152 public void deleteGroup(GroupIdV1 groupIdV1
) {
153 try (final var connection
= database
.getConnection()) {
154 deleteGroup(connection
, groupIdV1
);
155 } catch (SQLException e
) {
156 throw new RuntimeException("Failed update group store", e
);
160 private void deleteGroup(final Connection connection
, final GroupIdV1 groupIdV1
) throws SQLException
{
166 ).formatted(TABLE_GROUP_V1
);
167 try (final var statement
= connection
.prepareStatement(sql
)) {
168 statement
.setBytes(1, groupIdV1
.serialize());
169 statement
.executeUpdate();
173 public void deleteGroup(GroupIdV2 groupIdV2
) {
174 try (final var connection
= database
.getConnection()) {
180 ).formatted(TABLE_GROUP_V2
);
181 try (final var statement
= connection
.prepareStatement(sql
)) {
182 statement
.setBytes(1, groupIdV2
.serialize());
183 statement
.executeUpdate();
185 } catch (SQLException e
) {
186 throw new RuntimeException("Failed update group store", e
);
190 public GroupInfo
getGroup(GroupId groupId
) {
191 try (final var connection
= database
.getConnection()) {
192 return getGroup(connection
, groupId
);
193 } catch (SQLException e
) {
194 throw new RuntimeException("Failed read from group store", e
);
198 public GroupInfo
getGroup(final Connection connection
, final GroupId groupId
) throws SQLException
{
200 case GroupIdV1 groupIdV1
-> {
201 final var group
= getGroup(connection
, groupIdV1
);
205 return getGroupV2ByV1Id(connection
, groupIdV1
);
207 case GroupIdV2 groupIdV2
-> {
208 final var group
= getGroup(connection
, groupIdV2
);
212 return getGroupV1ByV2Id(connection
, groupIdV2
);
217 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
218 try (final var connection
= database
.getConnection()) {
219 var group
= getGroup(connection
, groupId
);
225 if (getGroupV2ByV1Id(connection
, groupId
) == null) {
226 return new GroupInfoV1(groupId
);
230 } catch (SQLException e
) {
231 throw new RuntimeException("Failed read from group store", e
);
235 public GroupInfoV2
getGroupOrPartialMigrate(
236 Connection connection
, final GroupMasterKey groupMasterKey
237 ) throws SQLException
{
238 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
239 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
241 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
244 public GroupInfoV2
getGroupOrPartialMigrate(
245 final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
247 try (final var connection
= database
.getConnection()) {
248 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
249 } catch (SQLException e
) {
250 throw new RuntimeException("Failed read from group store", e
);
254 private GroupInfoV2
getGroupOrPartialMigrate(
255 Connection connection
, final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
256 ) throws SQLException
{
257 switch (getGroup(groupId
)) {
258 case GroupInfoV1 groupInfoV1
-> {
259 // Received a v2 group message for a v1 group, we need to locally migrate the group
260 deleteGroup(connection
, groupInfoV1
.getGroupId());
261 final var groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
262 groupInfoV2
.setBlocked(groupInfoV1
.isBlocked());
263 updateGroup(connection
, groupInfoV2
);
264 logger
.debug("Locally migrated group {} to group v2, id: {}",
265 groupInfoV1
.getGroupId().toBase64(),
266 groupInfoV2
.getGroupId().toBase64());
269 case GroupInfoV2 groupInfoV2
-> {
273 return new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
278 public List
<GroupInfo
> getGroups() {
279 return Stream
.concat(getGroupsV2().stream(), getGroupsV1().stream()).toList();
282 public List
<GroupIdV1
> getGroupV1Ids(Connection connection
) throws SQLException
{
288 ).formatted(TABLE_GROUP_V1
);
289 try (final var statement
= connection
.prepareStatement(sql
)) {
290 return Utils
.executeQueryForStream(statement
, this::getGroupIdV1FromResultSet
)
291 .filter(Objects
::nonNull
)
296 public List
<GroupIdV2
> getGroupV2Ids(Connection connection
) throws SQLException
{
302 ).formatted(TABLE_GROUP_V2
);
303 try (final var statement
= connection
.prepareStatement(sql
)) {
304 return Utils
.executeQueryForStream(statement
, this::getGroupIdV2FromResultSet
)
305 .filter(Objects
::nonNull
)
310 public void mergeRecipients(
311 final Connection connection
, final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
312 ) throws SQLException
{
317 WHERE recipient_id = ?
319 ).formatted(TABLE_GROUP_V1_MEMBER
);
320 try (final var statement
= connection
.prepareStatement(sql
)) {
321 statement
.setLong(1, recipientId
.id());
322 statement
.setLong(2, toBeMergedRecipientId
.id());
323 final var updatedRows
= statement
.executeUpdate();
324 if (updatedRows
> 0) {
325 logger
.debug("Updated {} group members when merging recipients", updatedRows
);
330 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
331 final var storageIds
= new ArrayList
<StorageId
>();
334 FROM %s g WHERE g.storage_id IS NOT NULL
336 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
337 Utils
.executeQueryForStream(statement
, this::getGroupV1StorageIdFromResultSet
).forEach(storageIds
::add
);
339 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
340 Utils
.executeQueryForStream(statement
, this::getGroupV2StorageIdFromResultSet
).forEach(storageIds
::add
);
345 public void updateStorageIds(
346 Connection connection
, Map
<GroupIdV1
, StorageId
> storageIdV1Map
, Map
<GroupIdV2
, StorageId
> storageIdV2Map
347 ) throws SQLException
{
355 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
356 for (final var entry
: storageIdV1Map
.entrySet()) {
357 statement
.setBytes(1, entry
.getValue().getRaw());
358 statement
.setBytes(2, entry
.getKey().serialize());
359 statement
.executeUpdate();
362 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
363 for (final var entry
: storageIdV2Map
.entrySet()) {
364 statement
.setBytes(1, entry
.getValue().getRaw());
365 statement
.setBytes(2, entry
.getKey().serialize());
366 statement
.executeUpdate();
371 public void updateStorageId(
372 Connection connection
, GroupId groupId
, StorageId storageId
373 ) throws SQLException
{
380 ).formatted(groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
381 try (final var statement
= connection
.prepareStatement(sqlV1
)) {
382 statement
.setBytes(1, storageId
.getRaw());
383 statement
.setBytes(2, groupId
.serialize());
384 statement
.executeUpdate();
388 public void setMissingStorageIds() {
389 final var selectSql
= (
393 WHERE g.storage_id IS NULL
396 final var updateSql
= (
403 try (final var connection
= database
.getConnection()) {
404 connection
.setAutoCommit(false);
405 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V1
))) {
406 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV1FromResultSet
).toList();
407 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V1
))) {
408 for (final var groupId
: groupIds
) {
409 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
410 updateStmt
.setBytes(2, groupId
.serialize());
414 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V2
))) {
415 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV2FromResultSet
).toList();
416 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V2
))) {
417 for (final var groupId
: groupIds
) {
418 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
419 updateStmt
.setBytes(2, groupId
.serialize());
420 updateStmt
.executeUpdate();
425 } catch (SQLException e
) {
426 throw new RuntimeException("Failed update group store", e
);
430 void addLegacyGroups(final Collection
<GroupInfo
> groups
) {
431 logger
.debug("Migrating legacy groups to database");
432 long start
= System
.nanoTime();
433 try (final var connection
= database
.getConnection()) {
434 connection
.setAutoCommit(false);
435 for (final var group
: groups
) {
436 insertOrReplaceGroup(connection
, null, group
);
439 } catch (SQLException e
) {
440 throw new RuntimeException("Failed update group store", e
);
442 logger
.debug("Complete groups migration took {}ms", (System
.nanoTime() - start
) / 1000000);
445 private void insertOrReplaceGroup(
446 final Connection connection
, Long internalId
, final GroupInfo group
447 ) throws SQLException
{
448 if (group
instanceof GroupInfoV1 groupV1
) {
449 if (internalId
!= null) {
450 final var sqlDeleteMembers
= "DELETE FROM %s where group_id = ?".formatted(TABLE_GROUP_V1_MEMBER
);
451 try (final var statement
= connection
.prepareStatement(sqlDeleteMembers
)) {
452 statement
.setLong(1, internalId
);
453 statement
.executeUpdate();
457 INSERT OR REPLACE INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
458 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
460 """.formatted(TABLE_GROUP_V1
);
461 try (final var statement
= connection
.prepareStatement(sql
)) {
462 if (internalId
== null) {
463 statement
.setNull(1, Types
.NUMERIC
);
465 statement
.setLong(1, internalId
);
467 statement
.setBytes(2, groupV1
.getGroupId().serialize());
468 statement
.setBytes(3, groupV1
.getExpectedV2Id().serialize());
469 statement
.setString(4, groupV1
.getTitle());
470 statement
.setString(5, groupV1
.color
);
471 statement
.setLong(6, groupV1
.getMessageExpirationTimer());
472 statement
.setBoolean(7, groupV1
.isBlocked());
473 statement
.setBoolean(8, groupV1
.archived
);
474 statement
.setBytes(9, KeyUtils
.createRawStorageId());
475 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
477 if (internalId
== null) {
478 if (generatedKey
.isPresent()) {
479 internalId
= generatedKey
.get();
481 throw new RuntimeException("Failed to add new group to database");
485 final var sqlInsertMember
= """
486 INSERT OR REPLACE INTO %s (group_id, recipient_id)
488 """.formatted(TABLE_GROUP_V1_MEMBER
);
489 try (final var statement
= connection
.prepareStatement(sqlInsertMember
)) {
490 for (final var recipient
: groupV1
.getMembers()) {
491 statement
.setLong(1, internalId
);
492 statement
.setLong(2, recipient
.id());
493 statement
.executeUpdate();
496 } else if (group
instanceof GroupInfoV2 groupV2
) {
499 INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, distribution_id, storage_id)
500 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
502 ).formatted(TABLE_GROUP_V2
);
503 try (final var statement
= connection
.prepareStatement(sql
)) {
504 if (internalId
== null) {
505 statement
.setNull(1, Types
.NUMERIC
);
507 statement
.setLong(1, internalId
);
509 statement
.setBytes(2, groupV2
.getGroupId().serialize());
510 statement
.setBytes(3, groupV2
.getMasterKey().serialize());
511 if (groupV2
.getGroup() == null) {
512 statement
.setNull(4, Types
.NUMERIC
);
514 statement
.setBytes(4, groupV2
.getGroup().encode());
516 statement
.setBytes(5, UuidUtil
.toByteArray(groupV2
.getDistributionId().asUuid()));
517 statement
.setBoolean(6, groupV2
.isBlocked());
518 statement
.setBoolean(7, groupV2
.isPermissionDenied());
519 statement
.setBytes(8, KeyUtils
.createRawStorageId());
520 statement
.executeUpdate();
523 throw new AssertionError("Invalid group id type");
527 private List
<GroupInfoV2
> getGroupsV2() {
530 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
533 ).formatted(TABLE_GROUP_V2
);
534 try (final var connection
= database
.getConnection()) {
535 try (final var statement
= connection
.prepareStatement(sql
)) {
536 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV2FromResultSet
)
537 .filter(Objects
::nonNull
)
540 } catch (SQLException e
) {
541 throw new RuntimeException("Failed read from group store", e
);
545 public GroupInfoV2
getGroup(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
548 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
552 ).formatted(TABLE_GROUP_V2
);
553 try (final var statement
= connection
.prepareStatement(sql
)) {
554 statement
.setBytes(1, groupIdV2
.serialize());
555 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
559 public StorageId
getGroupStorageId(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
566 ).formatted(TABLE_GROUP_V2
);
567 try (final var statement
= connection
.prepareStatement(sql
)) {
568 statement
.setBytes(1, groupIdV2
.serialize());
569 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV2StorageIdFromResultSet
);
570 if (storageId
.isPresent()) {
571 return storageId
.get();
574 final var newStorageId
= StorageId
.forGroupV2(KeyUtils
.createRawStorageId());
575 updateStorageId(connection
, groupIdV2
, newStorageId
);
579 public GroupInfoV2
getGroupV2(Connection connection
, StorageId storageId
) throws SQLException
{
582 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
584 WHERE g.storage_id = ?
586 ).formatted(TABLE_GROUP_V2
);
587 try (final var statement
= connection
.prepareStatement(sql
)) {
588 statement
.setBytes(1, storageId
.getRaw());
589 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
593 private GroupIdV2
getGroupIdV2FromResultSet(ResultSet resultSet
) throws SQLException
{
594 final var groupId
= resultSet
.getBytes("group_id");
595 return GroupId
.v2(groupId
);
598 private GroupInfoV2
getGroupInfoV2FromResultSet(ResultSet resultSet
) throws SQLException
{
600 final var groupId
= resultSet
.getBytes("group_id");
601 final var masterKey
= resultSet
.getBytes("master_key");
602 final var groupData
= resultSet
.getBytes("group_data");
603 final var distributionId
= resultSet
.getBytes("distribution_id");
604 final var blocked
= resultSet
.getBoolean("blocked");
605 final var permissionDenied
= resultSet
.getBoolean("permission_denied");
606 final var storageRecord
= resultSet
.getBytes("storage_record");
607 return new GroupInfoV2(GroupId
.v2(groupId
),
608 new GroupMasterKey(masterKey
),
609 groupData
== null ?
null : DecryptedGroup
.ADAPTER
.decode(groupData
),
610 DistributionId
.from(UuidUtil
.parseOrThrow(distributionId
)),
615 } catch (InvalidInputException
| IOException e
) {
620 private StorageId
getGroupV1StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
621 final var storageId
= resultSet
.getBytes("storage_id");
622 return storageId
== null
623 ? StorageId
.forGroupV1(KeyUtils
.createRawStorageId())
624 : StorageId
.forGroupV1(storageId
);
627 private StorageId
getGroupV2StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
628 final var storageId
= resultSet
.getBytes("storage_id");
629 return storageId
== null
630 ? StorageId
.forGroupV2(KeyUtils
.createRawStorageId())
631 : StorageId
.forGroupV2(storageId
);
634 private List
<GroupInfoV1
> getGroupsV1() {
637 SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
640 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
641 try (final var connection
= database
.getConnection()) {
642 try (final var statement
= connection
.prepareStatement(sql
)) {
643 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV1FromResultSet
)
644 .filter(Objects
::nonNull
)
647 } catch (SQLException e
) {
648 throw new RuntimeException("Failed read from group store", e
);
652 public GroupInfoV1
getGroup(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
655 SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
659 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
660 try (final var statement
= connection
.prepareStatement(sql
)) {
661 statement
.setBytes(1, groupIdV1
.serialize());
662 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
666 public StorageId
getGroupStorageId(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
673 ).formatted(TABLE_GROUP_V1
);
674 try (final var statement
= connection
.prepareStatement(sql
)) {
675 statement
.setBytes(1, groupIdV1
.serialize());
676 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV1StorageIdFromResultSet
);
677 if (storageId
.isPresent()) {
678 return storageId
.get();
681 final var newStorageId
= StorageId
.forGroupV1(KeyUtils
.createRawStorageId());
682 updateStorageId(connection
, groupIdV1
, newStorageId
);
686 public GroupInfoV1
getGroupV1(Connection connection
, StorageId storageId
) throws SQLException
{
689 SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
691 WHERE g.storage_id = ?
693 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
694 try (final var statement
= connection
.prepareStatement(sql
)) {
695 statement
.setBytes(1, storageId
.getRaw());
696 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
700 private GroupIdV1
getGroupIdV1FromResultSet(ResultSet resultSet
) throws SQLException
{
701 final var groupId
= resultSet
.getBytes("group_id");
702 return GroupId
.v1(groupId
);
705 private GroupInfoV1
getGroupInfoV1FromResultSet(ResultSet resultSet
) throws SQLException
{
706 final var groupId
= resultSet
.getBytes("group_id");
707 final var groupIdV2
= resultSet
.getBytes("group_id_v2");
708 final var name
= resultSet
.getString("name");
709 final var color
= resultSet
.getString("color");
710 final var membersString
= resultSet
.getString("members");
711 final var members
= membersString
== null
712 ? Set
.<RecipientId
>of()
713 : Arrays
.stream(membersString
.split(","))
714 .map(Integer
::valueOf
)
715 .map(recipientIdCreator
::create
)
716 .collect(Collectors
.toSet());
717 final var expirationTime
= resultSet
.getInt("expiration_time");
718 final var blocked
= resultSet
.getBoolean("blocked");
719 final var archived
= resultSet
.getBoolean("archived");
720 final var storagRecord
= resultSet
.getBytes("storage_record");
721 return new GroupInfoV1(GroupId
.v1(groupId
),
722 groupIdV2
== null ?
null : GroupId
.v2(groupIdV2
),
732 private GroupInfoV2
getGroupV2ByV1Id(final Connection connection
, final GroupIdV1 groupId
) throws SQLException
{
733 return getGroup(connection
, GroupUtils
.getGroupIdV2(groupId
));
736 private GroupInfoV1
getGroupV1ByV2Id(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
739 SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
741 WHERE g.group_id_v2 = ?
743 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
744 try (final var statement
= connection
.prepareStatement(sql
)) {
745 statement
.setBytes(1, groupIdV2
.serialize());
746 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);