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
{
125 final var groupTable
= groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
;
126 final var deleteSql
= (
129 SET storage_id = NULL
132 ).formatted(groupTable
);
133 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
134 statement
.setBytes(1, storageId
.getRaw());
135 statement
.executeUpdate();
140 SET storage_id = ?, storage_record = ?
143 ).formatted(groupTable
);
144 try (final var statement
= connection
.prepareStatement(sql
)) {
145 statement
.setBytes(1, storageId
.getRaw());
146 if (storageRecord
== null) {
147 statement
.setNull(2, Types
.BLOB
);
149 statement
.setBytes(2, storageRecord
);
151 statement
.setBytes(3, groupId
.serialize());
152 statement
.executeUpdate();
156 public void deleteGroup(GroupId groupId
) {
157 if (groupId
instanceof GroupIdV1 groupIdV1
) {
158 deleteGroup(groupIdV1
);
159 } else if (groupId
instanceof GroupIdV2 groupIdV2
) {
160 deleteGroup(groupIdV2
);
164 public void deleteGroup(GroupIdV1 groupIdV1
) {
165 try (final var connection
= database
.getConnection()) {
166 deleteGroup(connection
, groupIdV1
);
167 } catch (SQLException e
) {
168 throw new RuntimeException("Failed update group store", e
);
172 private void deleteGroup(final Connection connection
, final GroupIdV1 groupIdV1
) throws SQLException
{
178 ).formatted(TABLE_GROUP_V1
);
179 try (final var statement
= connection
.prepareStatement(sql
)) {
180 statement
.setBytes(1, groupIdV1
.serialize());
181 statement
.executeUpdate();
185 public void deleteGroup(GroupIdV2 groupIdV2
) {
186 try (final var connection
= database
.getConnection()) {
192 ).formatted(TABLE_GROUP_V2
);
193 try (final var statement
= connection
.prepareStatement(sql
)) {
194 statement
.setBytes(1, groupIdV2
.serialize());
195 statement
.executeUpdate();
197 } catch (SQLException e
) {
198 throw new RuntimeException("Failed update group store", e
);
202 public GroupInfo
getGroup(GroupId groupId
) {
203 try (final var connection
= database
.getConnection()) {
204 return getGroup(connection
, groupId
);
205 } catch (SQLException e
) {
206 throw new RuntimeException("Failed read from group store", e
);
210 public GroupInfo
getGroup(final Connection connection
, final GroupId groupId
) throws SQLException
{
212 case GroupIdV1 groupIdV1
-> {
213 final var group
= getGroup(connection
, groupIdV1
);
217 return getGroupV2ByV1Id(connection
, groupIdV1
);
219 case GroupIdV2 groupIdV2
-> {
220 final var group
= getGroup(connection
, groupIdV2
);
224 return getGroupV1ByV2Id(connection
, groupIdV2
);
229 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
230 try (final var connection
= database
.getConnection()) {
231 var group
= getGroup(connection
, groupId
);
237 if (getGroupV2ByV1Id(connection
, groupId
) == null) {
238 return new GroupInfoV1(groupId
);
242 } catch (SQLException e
) {
243 throw new RuntimeException("Failed read from group store", e
);
247 public GroupInfoV2
getGroupOrPartialMigrate(
248 Connection connection
, final GroupMasterKey groupMasterKey
249 ) throws SQLException
{
250 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
251 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
253 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
256 public GroupInfoV2
getGroupOrPartialMigrate(
257 final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
259 try (final var connection
= database
.getConnection()) {
260 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
261 } catch (SQLException e
) {
262 throw new RuntimeException("Failed read from group store", e
);
266 private GroupInfoV2
getGroupOrPartialMigrate(
267 Connection connection
, final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
268 ) throws SQLException
{
269 switch (getGroup(groupId
)) {
270 case GroupInfoV1 groupInfoV1
-> {
271 // Received a v2 group message for a v1 group, we need to locally migrate the group
272 deleteGroup(connection
, groupInfoV1
.getGroupId());
273 final var groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
274 groupInfoV2
.setBlocked(groupInfoV1
.isBlocked());
275 updateGroup(connection
, groupInfoV2
);
276 logger
.debug("Locally migrated group {} to group v2, id: {}",
277 groupInfoV1
.getGroupId().toBase64(),
278 groupInfoV2
.getGroupId().toBase64());
281 case GroupInfoV2 groupInfoV2
-> {
285 return new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
290 public List
<GroupInfo
> getGroups() {
291 return Stream
.concat(getGroupsV2().stream(), getGroupsV1().stream()).toList();
294 public List
<GroupIdV1
> getGroupV1Ids(Connection connection
) throws SQLException
{
300 ).formatted(TABLE_GROUP_V1
);
301 try (final var statement
= connection
.prepareStatement(sql
)) {
302 return Utils
.executeQueryForStream(statement
, this::getGroupIdV1FromResultSet
)
303 .filter(Objects
::nonNull
)
308 public List
<GroupIdV2
> getGroupV2Ids(Connection connection
) throws SQLException
{
314 ).formatted(TABLE_GROUP_V2
);
315 try (final var statement
= connection
.prepareStatement(sql
)) {
316 return Utils
.executeQueryForStream(statement
, this::getGroupIdV2FromResultSet
)
317 .filter(Objects
::nonNull
)
322 public void mergeRecipients(
323 final Connection connection
, final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
324 ) throws SQLException
{
329 WHERE recipient_id = ?
331 ).formatted(TABLE_GROUP_V1_MEMBER
);
332 try (final var statement
= connection
.prepareStatement(sql
)) {
333 statement
.setLong(1, recipientId
.id());
334 statement
.setLong(2, toBeMergedRecipientId
.id());
335 final var updatedRows
= statement
.executeUpdate();
336 if (updatedRows
> 0) {
337 logger
.debug("Updated {} group members when merging recipients", updatedRows
);
342 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
343 final var storageIds
= new ArrayList
<StorageId
>();
346 FROM %s g WHERE g.storage_id IS NOT NULL
348 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
349 Utils
.executeQueryForStream(statement
, this::getGroupV1StorageIdFromResultSet
).forEach(storageIds
::add
);
351 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
352 Utils
.executeQueryForStream(statement
, this::getGroupV2StorageIdFromResultSet
).forEach(storageIds
::add
);
357 public void updateStorageIds(
358 Connection connection
, Map
<GroupIdV1
, StorageId
> storageIdV1Map
, Map
<GroupIdV2
, StorageId
> storageIdV2Map
359 ) throws SQLException
{
367 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
368 for (final var entry
: storageIdV1Map
.entrySet()) {
369 statement
.setBytes(1, entry
.getValue().getRaw());
370 statement
.setBytes(2, entry
.getKey().serialize());
371 statement
.executeUpdate();
374 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
375 for (final var entry
: storageIdV2Map
.entrySet()) {
376 statement
.setBytes(1, entry
.getValue().getRaw());
377 statement
.setBytes(2, entry
.getKey().serialize());
378 statement
.executeUpdate();
383 public void updateStorageId(
384 Connection connection
, GroupId groupId
, StorageId storageId
385 ) throws SQLException
{
392 ).formatted(groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
393 try (final var statement
= connection
.prepareStatement(sqlV1
)) {
394 statement
.setBytes(1, storageId
.getRaw());
395 statement
.setBytes(2, groupId
.serialize());
396 statement
.executeUpdate();
400 public void setMissingStorageIds() {
401 final var selectSql
= (
405 WHERE g.storage_id IS NULL
408 final var updateSql
= (
415 try (final var connection
= database
.getConnection()) {
416 connection
.setAutoCommit(false);
417 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V1
))) {
418 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV1FromResultSet
).toList();
419 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V1
))) {
420 for (final var groupId
: groupIds
) {
421 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
422 updateStmt
.setBytes(2, groupId
.serialize());
426 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V2
))) {
427 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV2FromResultSet
).toList();
428 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V2
))) {
429 for (final var groupId
: groupIds
) {
430 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
431 updateStmt
.setBytes(2, groupId
.serialize());
432 updateStmt
.executeUpdate();
437 } catch (SQLException e
) {
438 throw new RuntimeException("Failed update group store", e
);
442 void addLegacyGroups(final Collection
<GroupInfo
> groups
) {
443 logger
.debug("Migrating legacy groups to database");
444 long start
= System
.nanoTime();
445 try (final var connection
= database
.getConnection()) {
446 connection
.setAutoCommit(false);
447 for (final var group
: groups
) {
448 insertOrReplaceGroup(connection
, null, group
);
451 } catch (SQLException e
) {
452 throw new RuntimeException("Failed update group store", e
);
454 logger
.debug("Complete groups migration took {}ms", (System
.nanoTime() - start
) / 1000000);
457 private void insertOrReplaceGroup(
458 final Connection connection
, Long internalId
, final GroupInfo group
459 ) throws SQLException
{
460 if (group
instanceof GroupInfoV1 groupV1
) {
461 if (internalId
!= null) {
462 final var sqlDeleteMembers
= "DELETE FROM %s where group_id = ?".formatted(TABLE_GROUP_V1_MEMBER
);
463 try (final var statement
= connection
.prepareStatement(sqlDeleteMembers
)) {
464 statement
.setLong(1, internalId
);
465 statement
.executeUpdate();
469 INSERT OR REPLACE INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
470 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
472 """.formatted(TABLE_GROUP_V1
);
473 try (final var statement
= connection
.prepareStatement(sql
)) {
474 if (internalId
== null) {
475 statement
.setNull(1, Types
.NUMERIC
);
477 statement
.setLong(1, internalId
);
479 statement
.setBytes(2, groupV1
.getGroupId().serialize());
480 statement
.setBytes(3, groupV1
.getExpectedV2Id().serialize());
481 statement
.setString(4, groupV1
.getTitle());
482 statement
.setString(5, groupV1
.color
);
483 statement
.setLong(6, groupV1
.getMessageExpirationTimer());
484 statement
.setBoolean(7, groupV1
.isBlocked());
485 statement
.setBoolean(8, groupV1
.archived
);
486 statement
.setBytes(9, KeyUtils
.createRawStorageId());
487 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
489 if (internalId
== null) {
490 if (generatedKey
.isPresent()) {
491 internalId
= generatedKey
.get();
493 throw new RuntimeException("Failed to add new group to database");
497 final var sqlInsertMember
= """
498 INSERT OR REPLACE INTO %s (group_id, recipient_id)
500 """.formatted(TABLE_GROUP_V1_MEMBER
);
501 try (final var statement
= connection
.prepareStatement(sqlInsertMember
)) {
502 for (final var recipient
: groupV1
.getMembers()) {
503 statement
.setLong(1, internalId
);
504 statement
.setLong(2, recipient
.id());
505 statement
.executeUpdate();
508 } else if (group
instanceof GroupInfoV2 groupV2
) {
511 INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, distribution_id, storage_id)
512 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
514 ).formatted(TABLE_GROUP_V2
);
515 try (final var statement
= connection
.prepareStatement(sql
)) {
516 if (internalId
== null) {
517 statement
.setNull(1, Types
.NUMERIC
);
519 statement
.setLong(1, internalId
);
521 statement
.setBytes(2, groupV2
.getGroupId().serialize());
522 statement
.setBytes(3, groupV2
.getMasterKey().serialize());
523 if (groupV2
.getGroup() == null) {
524 statement
.setNull(4, Types
.NUMERIC
);
526 statement
.setBytes(4, groupV2
.getGroup().encode());
528 statement
.setBytes(5, UuidUtil
.toByteArray(groupV2
.getDistributionId().asUuid()));
529 statement
.setBoolean(6, groupV2
.isBlocked());
530 statement
.setBoolean(7, groupV2
.isPermissionDenied());
531 statement
.setBytes(8, KeyUtils
.createRawStorageId());
532 statement
.executeUpdate();
535 throw new AssertionError("Invalid group id type");
539 private List
<GroupInfoV2
> getGroupsV2() {
542 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
545 ).formatted(TABLE_GROUP_V2
);
546 try (final var connection
= database
.getConnection()) {
547 try (final var statement
= connection
.prepareStatement(sql
)) {
548 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV2FromResultSet
)
549 .filter(Objects
::nonNull
)
552 } catch (SQLException e
) {
553 throw new RuntimeException("Failed read from group store", e
);
557 public GroupInfoV2
getGroup(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
560 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
564 ).formatted(TABLE_GROUP_V2
);
565 try (final var statement
= connection
.prepareStatement(sql
)) {
566 statement
.setBytes(1, groupIdV2
.serialize());
567 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
571 public StorageId
getGroupStorageId(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
578 ).formatted(TABLE_GROUP_V2
);
579 try (final var statement
= connection
.prepareStatement(sql
)) {
580 statement
.setBytes(1, groupIdV2
.serialize());
581 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV2StorageIdFromResultSet
);
582 if (storageId
.isPresent()) {
583 return storageId
.get();
586 final var newStorageId
= StorageId
.forGroupV2(KeyUtils
.createRawStorageId());
587 updateStorageId(connection
, groupIdV2
, newStorageId
);
591 public GroupInfoV2
getGroupV2(Connection connection
, StorageId storageId
) throws SQLException
{
594 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied, g.storage_record
596 WHERE g.storage_id = ?
598 ).formatted(TABLE_GROUP_V2
);
599 try (final var statement
= connection
.prepareStatement(sql
)) {
600 statement
.setBytes(1, storageId
.getRaw());
601 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
605 private GroupIdV2
getGroupIdV2FromResultSet(ResultSet resultSet
) throws SQLException
{
606 final var groupId
= resultSet
.getBytes("group_id");
607 return GroupId
.v2(groupId
);
610 private GroupInfoV2
getGroupInfoV2FromResultSet(ResultSet resultSet
) throws SQLException
{
612 final var groupId
= resultSet
.getBytes("group_id");
613 final var masterKey
= resultSet
.getBytes("master_key");
614 final var groupData
= resultSet
.getBytes("group_data");
615 final var distributionId
= resultSet
.getBytes("distribution_id");
616 final var blocked
= resultSet
.getBoolean("blocked");
617 final var permissionDenied
= resultSet
.getBoolean("permission_denied");
618 final var storageRecord
= resultSet
.getBytes("storage_record");
619 return new GroupInfoV2(GroupId
.v2(groupId
),
620 new GroupMasterKey(masterKey
),
621 groupData
== null ?
null : DecryptedGroup
.ADAPTER
.decode(groupData
),
622 DistributionId
.from(UuidUtil
.parseOrThrow(distributionId
)),
627 } catch (InvalidInputException
| IOException e
) {
632 private StorageId
getGroupV1StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
633 final var storageId
= resultSet
.getBytes("storage_id");
634 return storageId
== null
635 ? StorageId
.forGroupV1(KeyUtils
.createRawStorageId())
636 : StorageId
.forGroupV1(storageId
);
639 private StorageId
getGroupV2StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
640 final var storageId
= resultSet
.getBytes("storage_id");
641 return storageId
== null
642 ? StorageId
.forGroupV2(KeyUtils
.createRawStorageId())
643 : StorageId
.forGroupV2(storageId
);
646 private List
<GroupInfoV1
> getGroupsV1() {
649 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
652 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
653 try (final var connection
= database
.getConnection()) {
654 try (final var statement
= connection
.prepareStatement(sql
)) {
655 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV1FromResultSet
)
656 .filter(Objects
::nonNull
)
659 } catch (SQLException e
) {
660 throw new RuntimeException("Failed read from group store", e
);
664 public GroupInfoV1
getGroup(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
667 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
671 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
672 try (final var statement
= connection
.prepareStatement(sql
)) {
673 statement
.setBytes(1, groupIdV1
.serialize());
674 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
678 public StorageId
getGroupStorageId(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
685 ).formatted(TABLE_GROUP_V1
);
686 try (final var statement
= connection
.prepareStatement(sql
)) {
687 statement
.setBytes(1, groupIdV1
.serialize());
688 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV1StorageIdFromResultSet
);
689 if (storageId
.isPresent()) {
690 return storageId
.get();
693 final var newStorageId
= StorageId
.forGroupV1(KeyUtils
.createRawStorageId());
694 updateStorageId(connection
, groupIdV1
, newStorageId
);
698 public GroupInfoV1
getGroupV1(Connection connection
, StorageId storageId
) throws SQLException
{
701 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
703 WHERE g.storage_id = ?
705 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
706 try (final var statement
= connection
.prepareStatement(sql
)) {
707 statement
.setBytes(1, storageId
.getRaw());
708 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
712 private GroupIdV1
getGroupIdV1FromResultSet(ResultSet resultSet
) throws SQLException
{
713 final var groupId
= resultSet
.getBytes("group_id");
714 return GroupId
.v1(groupId
);
717 private GroupInfoV1
getGroupInfoV1FromResultSet(ResultSet resultSet
) throws SQLException
{
718 final var groupId
= resultSet
.getBytes("group_id");
719 final var groupIdV2
= resultSet
.getBytes("group_id_v2");
720 final var name
= resultSet
.getString("name");
721 final var color
= resultSet
.getString("color");
722 final var membersString
= resultSet
.getString("members");
723 final var members
= membersString
== null
724 ? Set
.<RecipientId
>of()
725 : Arrays
.stream(membersString
.split(","))
726 .map(Integer
::valueOf
)
727 .map(recipientIdCreator
::create
)
728 .collect(Collectors
.toSet());
729 final var expirationTime
= resultSet
.getInt("expiration_time");
730 final var blocked
= resultSet
.getBoolean("blocked");
731 final var archived
= resultSet
.getBoolean("archived");
732 final var storagRecord
= resultSet
.getBytes("storage_record");
733 return new GroupInfoV1(GroupId
.v1(groupId
),
734 groupIdV2
== null ?
null : GroupId
.v2(groupIdV2
),
744 private GroupInfoV2
getGroupV2ByV1Id(final Connection connection
, final GroupIdV1 groupId
) throws SQLException
{
745 return getGroup(connection
, GroupUtils
.getGroupIdV2(groupId
));
748 private GroupInfoV1
getGroupV1ByV2Id(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
751 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
753 WHERE g.group_id_v2 = ?
755 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
756 try (final var statement
= connection
.prepareStatement(sql
)) {
757 statement
.setBytes(1, groupIdV2
.serialize());
758 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);