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 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
63 permission_denied INTEGER NOT NULL DEFAULT FALSE
65 CREATE TABLE group_v1 (
66 _id INTEGER PRIMARY KEY,
67 storage_id BLOB UNIQUE,
69 group_id BLOB UNIQUE NOT NULL,
70 group_id_v2 BLOB UNIQUE,
73 expiration_time INTEGER NOT NULL DEFAULT 0,
74 blocked INTEGER NOT NULL DEFAULT FALSE,
75 archived INTEGER NOT NULL DEFAULT FALSE
77 CREATE TABLE group_v1_member (
78 _id INTEGER PRIMARY KEY,
79 group_id INTEGER NOT NULL REFERENCES group_v1 (_id) ON DELETE CASCADE,
80 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
81 UNIQUE(group_id, recipient_id)
88 final Database database
,
89 final RecipientResolver recipientResolver
,
90 final RecipientIdCreator recipientIdCreator
92 this.database
= database
;
93 this.recipientResolver
= recipientResolver
;
94 this.recipientIdCreator
= recipientIdCreator
;
97 public void updateGroup(GroupInfo group
) {
98 try (final var connection
= database
.getConnection()) {
99 connection
.setAutoCommit(false);
100 updateGroup(connection
, group
);
102 } catch (SQLException e
) {
103 throw new RuntimeException("Failed update recipient store", e
);
107 public void updateGroup(final Connection connection
, final GroupInfo group
) throws SQLException
{
108 final Long internalId
;
115 ).formatted(group
instanceof GroupInfoV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
116 try (final var statement
= connection
.prepareStatement(sql
)) {
117 statement
.setBytes(1, group
.getGroupId().serialize());
118 internalId
= Utils
.executeQueryForOptional(statement
, res
-> res
.getLong("_id")).orElse(null);
120 insertOrReplaceGroup(connection
, internalId
, group
);
123 public void storeStorageRecord(
124 final Connection connection
, final GroupId groupId
, final StorageId storageId
, final byte[] storageRecord
125 ) throws SQLException
{
126 final var groupTable
= groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
;
127 final var deleteSql
= (
130 SET storage_id = NULL
133 ).formatted(groupTable
);
134 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
135 statement
.setBytes(1, storageId
.getRaw());
136 statement
.executeUpdate();
141 SET storage_id = ?, storage_record = ?
144 ).formatted(groupTable
);
145 try (final var statement
= connection
.prepareStatement(sql
)) {
146 statement
.setBytes(1, storageId
.getRaw());
147 if (storageRecord
== null) {
148 statement
.setNull(2, Types
.BLOB
);
150 statement
.setBytes(2, storageRecord
);
152 statement
.setBytes(3, groupId
.serialize());
153 statement
.executeUpdate();
157 public void deleteGroup(GroupId groupId
) {
158 if (groupId
instanceof GroupIdV1 groupIdV1
) {
159 deleteGroup(groupIdV1
);
160 } else if (groupId
instanceof GroupIdV2 groupIdV2
) {
161 deleteGroup(groupIdV2
);
165 public void deleteGroup(GroupIdV1 groupIdV1
) {
166 try (final var connection
= database
.getConnection()) {
167 deleteGroup(connection
, groupIdV1
);
168 } catch (SQLException e
) {
169 throw new RuntimeException("Failed update group store", e
);
173 private void deleteGroup(final Connection connection
, final GroupIdV1 groupIdV1
) throws SQLException
{
179 ).formatted(TABLE_GROUP_V1
);
180 try (final var statement
= connection
.prepareStatement(sql
)) {
181 statement
.setBytes(1, groupIdV1
.serialize());
182 statement
.executeUpdate();
186 public void deleteGroup(GroupIdV2 groupIdV2
) {
187 try (final var connection
= database
.getConnection()) {
193 ).formatted(TABLE_GROUP_V2
);
194 try (final var statement
= connection
.prepareStatement(sql
)) {
195 statement
.setBytes(1, groupIdV2
.serialize());
196 statement
.executeUpdate();
198 } catch (SQLException e
) {
199 throw new RuntimeException("Failed update group store", e
);
203 public GroupInfo
getGroup(GroupId groupId
) {
204 try (final var connection
= database
.getConnection()) {
205 return getGroup(connection
, groupId
);
206 } catch (SQLException e
) {
207 throw new RuntimeException("Failed read from group store", e
);
211 public GroupInfo
getGroup(final Connection connection
, final GroupId groupId
) throws SQLException
{
213 case GroupIdV1 groupIdV1
-> {
214 final var group
= getGroup(connection
, groupIdV1
);
218 return getGroupV2ByV1Id(connection
, groupIdV1
);
220 case GroupIdV2 groupIdV2
-> {
221 final var group
= getGroup(connection
, groupIdV2
);
225 return getGroupV1ByV2Id(connection
, groupIdV2
);
230 public GroupInfoV1
getOrCreateGroupV1(GroupIdV1 groupId
) {
231 try (final var connection
= database
.getConnection()) {
232 return getOrCreateGroupV1(connection
, groupId
);
233 } catch (SQLException e
) {
234 throw new RuntimeException("Failed read from group store", e
);
238 public GroupInfoV1
getOrCreateGroupV1(final Connection connection
, final GroupIdV1 groupId
) throws SQLException
{
239 var group
= getGroup(connection
, groupId
);
245 if (getGroupV2ByV1Id(connection
, groupId
) == null) {
246 return new GroupInfoV1(groupId
);
252 public GroupInfoV2
getGroupOrPartialMigrate(
253 Connection connection
, final GroupMasterKey groupMasterKey
254 ) throws SQLException
{
255 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
256 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
258 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
261 public GroupInfoV2
getGroupOrPartialMigrate(
262 final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
264 try (final var connection
= database
.getConnection()) {
265 return getGroupOrPartialMigrate(connection
, groupMasterKey
, groupId
);
266 } catch (SQLException e
) {
267 throw new RuntimeException("Failed read from group store", e
);
271 private GroupInfoV2
getGroupOrPartialMigrate(
272 Connection connection
, final GroupMasterKey groupMasterKey
, final GroupIdV2 groupId
273 ) throws SQLException
{
274 switch (getGroup(groupId
)) {
275 case GroupInfoV1 groupInfoV1
-> {
276 // Received a v2 group message for a v1 group, we need to locally migrate the group
277 deleteGroup(connection
, groupInfoV1
.getGroupId());
278 final var groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
279 groupInfoV2
.setBlocked(groupInfoV1
.isBlocked());
280 updateGroup(connection
, groupInfoV2
);
281 logger
.debug("Locally migrated group {} to group v2, id: {}",
282 groupInfoV1
.getGroupId().toBase64(),
283 groupInfoV2
.getGroupId().toBase64());
286 case GroupInfoV2 groupInfoV2
-> {
290 return new GroupInfoV2(groupId
, groupMasterKey
, recipientResolver
);
295 public List
<GroupInfo
> getGroups() {
296 return Stream
.concat(getGroupsV2().stream(), getGroupsV1().stream()).toList();
299 public List
<GroupIdV1
> getGroupV1Ids(Connection connection
) throws SQLException
{
305 ).formatted(TABLE_GROUP_V1
);
306 try (final var statement
= connection
.prepareStatement(sql
)) {
307 return Utils
.executeQueryForStream(statement
, this::getGroupIdV1FromResultSet
)
308 .filter(Objects
::nonNull
)
313 public List
<GroupIdV2
> getGroupV2Ids(Connection connection
) throws SQLException
{
319 ).formatted(TABLE_GROUP_V2
);
320 try (final var statement
= connection
.prepareStatement(sql
)) {
321 return Utils
.executeQueryForStream(statement
, this::getGroupIdV2FromResultSet
)
322 .filter(Objects
::nonNull
)
327 public void mergeRecipients(
328 final Connection connection
, final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
329 ) throws SQLException
{
334 WHERE recipient_id = ?
336 ).formatted(TABLE_GROUP_V1_MEMBER
);
337 try (final var statement
= connection
.prepareStatement(sql
)) {
338 statement
.setLong(1, recipientId
.id());
339 statement
.setLong(2, toBeMergedRecipientId
.id());
340 final var updatedRows
= statement
.executeUpdate();
341 if (updatedRows
> 0) {
342 logger
.debug("Updated {} group members when merging recipients", updatedRows
);
347 public List
<StorageId
> getStorageIds(Connection connection
) throws SQLException
{
348 final var storageIds
= new ArrayList
<StorageId
>();
351 FROM %s g WHERE g.storage_id IS NOT NULL
353 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
354 Utils
.executeQueryForStream(statement
, this::getGroupV1StorageIdFromResultSet
).forEach(storageIds
::add
);
356 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
357 Utils
.executeQueryForStream(statement
, this::getGroupV2StorageIdFromResultSet
).forEach(storageIds
::add
);
362 public void updateStorageIds(
363 Connection connection
, Map
<GroupIdV1
, StorageId
> storageIdV1Map
, Map
<GroupIdV2
, StorageId
> storageIdV2Map
364 ) throws SQLException
{
372 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V1
))) {
373 for (final var entry
: storageIdV1Map
.entrySet()) {
374 statement
.setBytes(1, entry
.getValue().getRaw());
375 statement
.setBytes(2, entry
.getKey().serialize());
376 statement
.executeUpdate();
379 try (final var statement
= connection
.prepareStatement(sql
.formatted(TABLE_GROUP_V2
))) {
380 for (final var entry
: storageIdV2Map
.entrySet()) {
381 statement
.setBytes(1, entry
.getValue().getRaw());
382 statement
.setBytes(2, entry
.getKey().serialize());
383 statement
.executeUpdate();
388 public void updateStorageId(
389 Connection connection
, GroupId groupId
, StorageId storageId
390 ) throws SQLException
{
397 ).formatted(groupId
instanceof GroupIdV1 ? TABLE_GROUP_V1
: TABLE_GROUP_V2
);
398 try (final var statement
= connection
.prepareStatement(sqlV1
)) {
399 statement
.setBytes(1, storageId
.getRaw());
400 statement
.setBytes(2, groupId
.serialize());
401 statement
.executeUpdate();
405 public void setMissingStorageIds() {
406 final var selectSql
= (
410 WHERE g.storage_id IS NULL
413 final var updateSql
= (
420 try (final var connection
= database
.getConnection()) {
421 connection
.setAutoCommit(false);
422 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V1
))) {
423 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV1FromResultSet
).toList();
424 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V1
))) {
425 for (final var groupId
: groupIds
) {
426 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
427 updateStmt
.setBytes(2, groupId
.serialize());
431 try (final var selectStmt
= connection
.prepareStatement(selectSql
.formatted(TABLE_GROUP_V2
))) {
432 final var groupIds
= Utils
.executeQueryForStream(selectStmt
, this::getGroupIdV2FromResultSet
).toList();
433 try (final var updateStmt
= connection
.prepareStatement(updateSql
.formatted(TABLE_GROUP_V2
))) {
434 for (final var groupId
: groupIds
) {
435 updateStmt
.setBytes(1, KeyUtils
.createRawStorageId());
436 updateStmt
.setBytes(2, groupId
.serialize());
437 updateStmt
.executeUpdate();
442 } catch (SQLException e
) {
443 throw new RuntimeException("Failed update group store", e
);
447 void addLegacyGroups(final Collection
<GroupInfo
> groups
) {
448 logger
.debug("Migrating legacy groups to database");
449 long start
= System
.nanoTime();
450 try (final var connection
= database
.getConnection()) {
451 connection
.setAutoCommit(false);
452 for (final var group
: groups
) {
453 insertOrReplaceGroup(connection
, null, group
);
456 } catch (SQLException e
) {
457 throw new RuntimeException("Failed update group store", e
);
459 logger
.debug("Complete groups migration took {}ms", (System
.nanoTime() - start
) / 1000000);
462 private void insertOrReplaceGroup(
463 final Connection connection
, Long internalId
, final GroupInfo group
464 ) throws SQLException
{
465 if (group
instanceof GroupInfoV1 groupV1
) {
466 if (internalId
!= null) {
467 final var sqlDeleteMembers
= "DELETE FROM %s where group_id = ?".formatted(TABLE_GROUP_V1_MEMBER
);
468 try (final var statement
= connection
.prepareStatement(sqlDeleteMembers
)) {
469 statement
.setLong(1, internalId
);
470 statement
.executeUpdate();
474 INSERT OR REPLACE INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
475 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
477 """.formatted(TABLE_GROUP_V1
);
478 try (final var statement
= connection
.prepareStatement(sql
)) {
479 if (internalId
== null) {
480 statement
.setNull(1, Types
.NUMERIC
);
482 statement
.setLong(1, internalId
);
484 statement
.setBytes(2, groupV1
.getGroupId().serialize());
485 statement
.setBytes(3, groupV1
.getExpectedV2Id().serialize());
486 statement
.setString(4, groupV1
.getTitle());
487 statement
.setString(5, groupV1
.color
);
488 statement
.setLong(6, groupV1
.getMessageExpirationTimer());
489 statement
.setBoolean(7, groupV1
.isBlocked());
490 statement
.setBoolean(8, groupV1
.archived
);
491 statement
.setBytes(9, KeyUtils
.createRawStorageId());
492 final var generatedKey
= Utils
.executeQueryForOptional(statement
, Utils
::getIdMapper
);
494 if (internalId
== null) {
495 if (generatedKey
.isPresent()) {
496 internalId
= generatedKey
.get();
498 throw new RuntimeException("Failed to add new group to database");
502 final var sqlInsertMember
= """
503 INSERT OR REPLACE INTO %s (group_id, recipient_id)
505 """.formatted(TABLE_GROUP_V1_MEMBER
);
506 try (final var statement
= connection
.prepareStatement(sqlInsertMember
)) {
507 for (final var recipient
: groupV1
.getMembers()) {
508 statement
.setLong(1, internalId
);
509 statement
.setLong(2, recipient
.id());
510 statement
.executeUpdate();
513 } else if (group
instanceof GroupInfoV2 groupV2
) {
516 INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
517 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
519 ).formatted(TABLE_GROUP_V2
);
520 try (final var statement
= connection
.prepareStatement(sql
)) {
521 if (internalId
== null) {
522 statement
.setNull(1, Types
.NUMERIC
);
524 statement
.setLong(1, internalId
);
526 statement
.setBytes(2, groupV2
.getGroupId().serialize());
527 statement
.setBytes(3, groupV2
.getMasterKey().serialize());
528 if (groupV2
.getGroup() == null) {
529 statement
.setNull(4, Types
.NUMERIC
);
531 statement
.setBytes(4, groupV2
.getGroup().encode());
533 statement
.setBytes(5, UuidUtil
.toByteArray(groupV2
.getDistributionId().asUuid()));
534 statement
.setBoolean(6, groupV2
.isBlocked());
535 statement
.setBoolean(7, groupV2
.isPermissionDenied());
536 statement
.setBytes(8, KeyUtils
.createRawStorageId());
537 statement
.setBoolean(9, groupV2
.isProfileSharingEnabled());
538 statement
.executeUpdate();
541 throw new AssertionError("Invalid group id type");
545 private List
<GroupInfoV2
> getGroupsV2() {
548 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
551 ).formatted(TABLE_GROUP_V2
);
552 try (final var connection
= database
.getConnection()) {
553 try (final var statement
= connection
.prepareStatement(sql
)) {
554 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV2FromResultSet
)
555 .filter(Objects
::nonNull
)
558 } catch (SQLException e
) {
559 throw new RuntimeException("Failed read from group store", e
);
563 public GroupInfoV2
getGroup(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
566 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
570 ).formatted(TABLE_GROUP_V2
);
571 try (final var statement
= connection
.prepareStatement(sql
)) {
572 statement
.setBytes(1, groupIdV2
.serialize());
573 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
577 public StorageId
getGroupStorageId(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
584 ).formatted(TABLE_GROUP_V2
);
585 try (final var statement
= connection
.prepareStatement(sql
)) {
586 statement
.setBytes(1, groupIdV2
.serialize());
587 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV2StorageIdFromResultSet
);
588 if (storageId
.isPresent()) {
589 return storageId
.get();
592 final var newStorageId
= StorageId
.forGroupV2(KeyUtils
.createRawStorageId());
593 updateStorageId(connection
, groupIdV2
, newStorageId
);
597 public GroupInfoV2
getGroupV2(Connection connection
, StorageId storageId
) throws SQLException
{
600 SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
602 WHERE g.storage_id = ?
604 ).formatted(TABLE_GROUP_V2
);
605 try (final var statement
= connection
.prepareStatement(sql
)) {
606 statement
.setBytes(1, storageId
.getRaw());
607 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV2FromResultSet
).orElse(null);
611 private GroupIdV2
getGroupIdV2FromResultSet(ResultSet resultSet
) throws SQLException
{
612 final var groupId
= resultSet
.getBytes("group_id");
613 return GroupId
.v2(groupId
);
616 private GroupInfoV2
getGroupInfoV2FromResultSet(ResultSet resultSet
) throws SQLException
{
618 final var groupId
= resultSet
.getBytes("group_id");
619 final var masterKey
= resultSet
.getBytes("master_key");
620 final var groupData
= resultSet
.getBytes("group_data");
621 final var distributionId
= resultSet
.getBytes("distribution_id");
622 final var blocked
= resultSet
.getBoolean("blocked");
623 final var profileSharingEnabled
= resultSet
.getBoolean("profile_sharing");
624 final var permissionDenied
= resultSet
.getBoolean("permission_denied");
625 final var storageRecord
= resultSet
.getBytes("storage_record");
626 return new GroupInfoV2(GroupId
.v2(groupId
),
627 new GroupMasterKey(masterKey
),
628 groupData
== null ?
null : DecryptedGroup
.ADAPTER
.decode(groupData
),
629 DistributionId
.from(UuidUtil
.parseOrThrow(distributionId
)),
631 profileSharingEnabled
,
635 } catch (InvalidInputException
| IOException e
) {
640 private StorageId
getGroupV1StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
641 final var storageId
= resultSet
.getBytes("storage_id");
642 return storageId
== null
643 ? StorageId
.forGroupV1(KeyUtils
.createRawStorageId())
644 : StorageId
.forGroupV1(storageId
);
647 private StorageId
getGroupV2StorageIdFromResultSet(ResultSet resultSet
) throws SQLException
{
648 final var storageId
= resultSet
.getBytes("storage_id");
649 return storageId
== null
650 ? StorageId
.forGroupV2(KeyUtils
.createRawStorageId())
651 : StorageId
.forGroupV2(storageId
);
654 private List
<GroupInfoV1
> getGroupsV1() {
657 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
660 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
661 try (final var connection
= database
.getConnection()) {
662 try (final var statement
= connection
.prepareStatement(sql
)) {
663 return Utils
.executeQueryForStream(statement
, this::getGroupInfoV1FromResultSet
)
664 .filter(Objects
::nonNull
)
667 } catch (SQLException e
) {
668 throw new RuntimeException("Failed read from group store", e
);
672 public GroupInfoV1
getGroup(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
675 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
679 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
680 try (final var statement
= connection
.prepareStatement(sql
)) {
681 statement
.setBytes(1, groupIdV1
.serialize());
682 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
686 public StorageId
getGroupStorageId(Connection connection
, GroupIdV1 groupIdV1
) throws SQLException
{
693 ).formatted(TABLE_GROUP_V1
);
694 try (final var statement
= connection
.prepareStatement(sql
)) {
695 statement
.setBytes(1, groupIdV1
.serialize());
696 final var storageId
= Utils
.executeQueryForOptional(statement
, this::getGroupV1StorageIdFromResultSet
);
697 if (storageId
.isPresent()) {
698 return storageId
.get();
701 final var newStorageId
= StorageId
.forGroupV1(KeyUtils
.createRawStorageId());
702 updateStorageId(connection
, groupIdV1
, newStorageId
);
706 public GroupInfoV1
getGroupV1(Connection connection
, StorageId storageId
) throws SQLException
{
709 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
711 WHERE g.storage_id = ?
713 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
714 try (final var statement
= connection
.prepareStatement(sql
)) {
715 statement
.setBytes(1, storageId
.getRaw());
716 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);
720 private GroupIdV1
getGroupIdV1FromResultSet(ResultSet resultSet
) throws SQLException
{
721 final var groupId
= resultSet
.getBytes("group_id");
722 return GroupId
.v1(groupId
);
725 private GroupInfoV1
getGroupInfoV1FromResultSet(ResultSet resultSet
) throws SQLException
{
726 final var groupId
= resultSet
.getBytes("group_id");
727 final var groupIdV2
= resultSet
.getBytes("group_id_v2");
728 final var name
= resultSet
.getString("name");
729 final var color
= resultSet
.getString("color");
730 final var membersString
= resultSet
.getString("members");
731 final var members
= membersString
== null
732 ? Set
.<RecipientId
>of()
733 : Arrays
.stream(membersString
.split(","))
734 .map(Integer
::valueOf
)
735 .map(recipientIdCreator
::create
)
736 .collect(Collectors
.toSet());
737 final var expirationTime
= resultSet
.getInt("expiration_time");
738 final var blocked
= resultSet
.getBoolean("blocked");
739 final var archived
= resultSet
.getBoolean("archived");
740 final var storageRecord
= resultSet
.getBytes("storage_record");
741 return new GroupInfoV1(GroupId
.v1(groupId
),
742 groupIdV2
== null ?
null : GroupId
.v2(groupIdV2
),
752 private GroupInfoV2
getGroupV2ByV1Id(final Connection connection
, final GroupIdV1 groupId
) throws SQLException
{
753 return getGroup(connection
, GroupUtils
.getGroupIdV2(groupId
));
756 private GroupInfoV1
getGroupV1ByV2Id(Connection connection
, GroupIdV2 groupIdV2
) throws SQLException
{
759 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
761 WHERE g.group_id_v2 = ?
763 ).formatted(TABLE_GROUP_V1_MEMBER
, TABLE_GROUP_V1
);
764 try (final var statement
= connection
.prepareStatement(sql
)) {
765 statement
.setBytes(1, groupIdV2
.serialize());
766 return Utils
.executeQueryForOptional(statement
, this::getGroupInfoV1FromResultSet
).orElse(null);