That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.ContentValues
4import android.content.Context
5import android.database.Cursor
6import android.text.TextUtils
7import androidx.annotation.WorkerThread
8import androidx.core.content.contentValuesOf
9import okio.ByteString
10import org.intellij.lang.annotations.Language
11import org.signal.core.util.SqlUtil
12import org.signal.core.util.SqlUtil.appendArg
13import org.signal.core.util.SqlUtil.buildArgs
14import org.signal.core.util.SqlUtil.buildCaseInsensitiveGlobPattern
15import org.signal.core.util.SqlUtil.buildCollectionQuery
16import org.signal.core.util.delete
17import org.signal.core.util.exists
18import org.signal.core.util.isAbsent
19import org.signal.core.util.logging.Log
20import org.signal.core.util.optionalString
21import org.signal.core.util.readToList
22import org.signal.core.util.readToSingleInt
23import org.signal.core.util.readToSingleObject
24import org.signal.core.util.requireBlob
25import org.signal.core.util.requireBoolean
26import org.signal.core.util.requireInt
27import org.signal.core.util.requireLong
28import org.signal.core.util.requireNonNullString
29import org.signal.core.util.requireString
30import org.signal.core.util.select
31import org.signal.core.util.update
32import org.signal.core.util.withinTransaction
33import org.signal.libsignal.zkgroup.groups.GroupMasterKey
34import org.signal.storageservice.protos.groups.Member
35import org.signal.storageservice.protos.groups.local.DecryptedGroup
36import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
37import org.tm.archive.contacts.paged.ContactSearchSortOrder
38import org.tm.archive.contacts.paged.collections.ContactSearchIterator
39import org.tm.archive.crypto.SenderKeyUtil
40import org.tm.archive.database.SignalDatabase.Companion.recipients
41import org.tm.archive.database.model.GroupRecord
42import org.tm.archive.dependencies.ApplicationDependencies
43import org.tm.archive.groups.BadGroupIdException
44import org.tm.archive.groups.GroupId
45import org.tm.archive.groups.GroupId.Push
46import org.tm.archive.groups.v2.processing.GroupsV2StateProcessor
47import org.tm.archive.jobs.RequestGroupV2InfoJob
48import org.tm.archive.keyvalue.SignalStore
49import org.tm.archive.recipients.Recipient
50import org.tm.archive.recipients.RecipientId
51import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
52import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
53import org.whispersystems.signalservice.api.groupsv2.findMemberByAci
54import org.whispersystems.signalservice.api.groupsv2.findPendingByServiceId
55import org.whispersystems.signalservice.api.groupsv2.findRequestingByAci
56import org.whispersystems.signalservice.api.groupsv2.toAciList
57import org.whispersystems.signalservice.api.groupsv2.toAciListWithUnknowns
58import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
59import org.whispersystems.signalservice.api.push.DistributionId
60import org.whispersystems.signalservice.api.push.ServiceId
61import org.whispersystems.signalservice.api.push.ServiceId.ACI
62import org.whispersystems.signalservice.api.push.ServiceId.ACI.Companion.parseOrNull
63import org.whispersystems.signalservice.api.push.ServiceId.PNI
64import java.io.Closeable
65import java.security.SecureRandom
66import java.util.Optional
67import java.util.stream.Collectors
68import javax.annotation.CheckReturnValue
69
70class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
71
72 companion object {
73 private val TAG = Log.tag(GroupTable::class.java)
74
75 const val MEMBER_GROUP_CONCAT = "member_group_concat"
76 const val THREAD_DATE = "thread_date"
77
78 const val TABLE_NAME = "groups"
79 const val ID = "_id"
80 const val GROUP_ID = "group_id"
81 const val RECIPIENT_ID = "recipient_id"
82 const val TITLE = "title"
83 const val AVATAR_ID = "avatar_id"
84 const val AVATAR_KEY = "avatar_key"
85 const val AVATAR_CONTENT_TYPE = "avatar_content_type"
86 const val AVATAR_DIGEST = "avatar_digest"
87 const val TIMESTAMP = "timestamp"
88 const val ACTIVE = "active"
89 const val MMS = "mms"
90 const val EXPECTED_V2_ID = "expected_v2_id"
91 const val UNMIGRATED_V1_MEMBERS = "unmigrated_v1_members"
92 const val DISTRIBUTION_ID = "distribution_id"
93 const val SHOW_AS_STORY_STATE = "show_as_story_state"
94 const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp"
95
96 /** 32 bytes serialized [GroupMasterKey] */
97 const val V2_MASTER_KEY = "master_key"
98
99 /** Increments with every change to the group */
100 const val V2_REVISION = "revision"
101
102 /** Serialized [DecryptedGroup] protobuf */
103 const val V2_DECRYPTED_GROUP = "decrypted_group"
104
105 @JvmField
106 val CREATE_TABLE = """
107 CREATE TABLE $TABLE_NAME (
108 $ID INTEGER PRIMARY KEY,
109 $GROUP_ID TEXT NOT NULL UNIQUE,
110 $RECIPIENT_ID INTEGER NOT NULL UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
111 $TITLE TEXT DEFAULT NULL,
112 $AVATAR_ID INTEGER DEFAULT 0,
113 $AVATAR_KEY BLOB DEFAULT NULL,
114 $AVATAR_CONTENT_TYPE TEXT DEFAULT NULL,
115 $AVATAR_DIGEST BLOB DEFAULT NULL,
116 $TIMESTAMP INTEGER DEFAULT 0,
117 $ACTIVE INTEGER DEFAULT 1,
118 $MMS INTEGER DEFAULT 0,
119 $V2_MASTER_KEY BLOB DEFAULT NULL,
120 $V2_REVISION BLOB DEFAULT NULL,
121 $V2_DECRYPTED_GROUP BLOB DEFAULT NULL,
122 $EXPECTED_V2_ID TEXT UNIQUE DEFAULT NULL,
123 $UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL,
124 $DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL,
125 $SHOW_AS_STORY_STATE INTEGER DEFAULT ${ShowAsStoryState.IF_ACTIVE.code},
126 $LAST_FORCE_UPDATE_TIMESTAMP INTEGER DEFAULT 0
127 )
128 """
129
130 @JvmField
131 val CREATE_INDEXS = MembershipTable.CREATE_INDEXES
132
133 private val GROUP_PROJECTION = arrayOf(
134 GROUP_ID,
135 RECIPIENT_ID,
136 TITLE,
137 UNMIGRATED_V1_MEMBERS,
138 AVATAR_ID,
139 AVATAR_KEY,
140 AVATAR_CONTENT_TYPE,
141 AVATAR_DIGEST,
142 TIMESTAMP,
143 ACTIVE,
144 MMS,
145 V2_MASTER_KEY,
146 V2_REVISION,
147 V2_DECRYPTED_GROUP,
148 LAST_FORCE_UPDATE_TIMESTAMP
149 )
150
151 val TYPED_GROUP_PROJECTION = GROUP_PROJECTION
152 .filterNot { it == RECIPIENT_ID }
153 .map { columnName: String -> "$TABLE_NAME.$columnName" }
154 .toList()
155
156 //language=sql
157 private val JOINED_GROUP_SELECT = """
158 SELECT
159 DISTINCT $TABLE_NAME.*,
160 (
161 SELECT GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID})
162 FROM ${MembershipTable.TABLE_NAME}
163 WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
164 ) as $MEMBER_GROUP_CONCAT
165 FROM $TABLE_NAME
166 """
167
168 val CREATE_TABLES = arrayOf(CREATE_TABLE, MembershipTable.CREATE_TABLE)
169 }
170
171 class MembershipTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
172 companion object {
173 const val TABLE_NAME = "group_membership"
174
175 const val ID = "_id"
176 const val GROUP_ID = "group_id"
177 const val RECIPIENT_ID = "recipient_id"
178
179 //language=sql
180 @JvmField
181 val CREATE_TABLE = """
182 CREATE TABLE $TABLE_NAME (
183 $ID INTEGER PRIMARY KEY,
184 $GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE,
185 $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
186 UNIQUE($GROUP_ID, $RECIPIENT_ID)
187 )
188 """
189
190 val CREATE_INDEXES = arrayOf(
191 "CREATE INDEX IF NOT EXISTS group_membership_recipient_id ON $TABLE_NAME ($RECIPIENT_ID)"
192 )
193 }
194 }
195
196 fun getGroup(recipientId: RecipientId): Optional<GroupRecord> {
197 return getGroup(SqlUtil.Query("$TABLE_NAME.$RECIPIENT_ID = ?", buildArgs(recipientId)))
198 }
199
200 fun getGroup(groupId: GroupId): Optional<GroupRecord> {
201 return getGroup(SqlUtil.Query("$TABLE_NAME.$GROUP_ID = ?", buildArgs(groupId)))
202 }
203
204 private fun getGroup(query: SqlUtil.Query): Optional<GroupRecord> {
205 //language=sql
206 val select = "$JOINED_GROUP_SELECT WHERE ${query.where}"
207
208 readableDatabase
209 .query(select, query.whereArgs)
210 .use { cursor ->
211 return if (cursor.moveToFirst()) {
212 val groupRecord = getGroup(cursor)
213 if (groupRecord.isPresent && RemappedRecords.getInstance().areAnyRemapped(groupRecord.get().members)) {
214 val groupId = groupRecord.get().id
215 val remaps = RemappedRecords.getInstance().buildRemapDescription(groupRecord.get().members)
216 Log.w(TAG, "Found a group with remapped recipients in it's membership list! Updating the list. GroupId: $groupId, Remaps: $remaps", true)
217
218 val oldToNew: List<Pair<RecipientId, RecipientId>> = groupRecord.get().members
219 .map { it to RemappedRecords.getInstance().getRecipient(it).orElse(null) }
220 .filterNot { (old, new) -> new == null || old == new }
221
222 if (oldToNew.isNotEmpty()) {
223 writableDatabase.withinTransaction { db ->
224 oldToNew.forEach { remapRecipient(it.first, it.second) }
225 }
226 }
227
228 readableDatabase.query(select, query.whereArgs).use { refreshedCursor ->
229 if (refreshedCursor.moveToFirst()) {
230 getGroup(refreshedCursor)
231 } else {
232 Optional.empty()
233 }
234 }
235 } else {
236 getGroup(cursor)
237 }
238 } else {
239 Optional.empty()
240 }
241 }
242 }
243
244 /**
245 * Call if you are sure this group should exist.
246 * Finds group and throws if it cannot.
247 */
248 fun requireGroup(groupId: GroupId): GroupRecord {
249 val group = getGroup(groupId)
250 if (!group.isPresent) {
251 throw AssertionError("Group not found")
252 }
253 return group.get()
254 }
255
256 fun groupExists(groupId: GroupId): Boolean {
257 return readableDatabase
258 .exists(TABLE_NAME)
259 .where("$GROUP_ID = ?", groupId.toString())
260 .run()
261 }
262
263 /**
264 * @return A gv1 group whose expected v2 ID matches the one provided.
265 */
266 fun getGroupV1ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
267 return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ?", buildArgs(gv2Id)))
268 }
269
270 /**
271 * @return A gv1 group whose expected v2 ID matches the one provided or a gv2 group whose ID matches the one provided.
272 *
273 * If a gv1 group is present, it will be returned first.
274 */
275 fun getGroupV1OrV2ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
276 return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ? OR $TABLE_NAME.$GROUP_ID = ? ORDER BY $TABLE_NAME.$EXPECTED_V2_ID DESC", buildArgs(gv2Id, gv2Id)))
277 }
278
279 fun getGroupByDistributionId(distributionId: DistributionId): Optional<GroupRecord> {
280 return getGroup(SqlUtil.Query("$TABLE_NAME.$DISTRIBUTION_ID = ?", buildArgs(distributionId)))
281 }
282
283 fun removeUnmigratedV1Members(id: GroupId.V2) {
284 val group = getGroup(id)
285 if (!group.isPresent) {
286 Log.w(TAG, "Couldn't find the group!", Throwable())
287 return
288 }
289
290 removeUnmigratedV1Members(id, group.get().unmigratedV1Members)
291 }
292
293 /**
294 * Removes the specified members from the list of 'unmigrated V1 members' -- the list of members
295 * that were either dropped or had to be invited when migrating the group from V1->V2.
296 */
297 fun removeUnmigratedV1Members(id: GroupId.V2, toRemove: List<RecipientId>) {
298 val group = getGroup(id)
299 if (group.isAbsent()) {
300 Log.w(TAG, "Couldn't find the group!", Throwable())
301 return
302 }
303
304 val newUnmigrated = group.get().unmigratedV1Members - toRemove.toSet()
305
306 writableDatabase
307 .update(TABLE_NAME)
308 .values(UNMIGRATED_V1_MEMBERS to if (newUnmigrated.isEmpty()) null else newUnmigrated.serialize())
309 .where("$GROUP_ID = ?", id)
310 .run()
311
312 Recipient.live(Recipient.externalGroupExact(id).id).refresh()
313 }
314
315 private fun getGroup(cursor: Cursor?): Optional<GroupRecord> {
316 val reader = Reader(cursor)
317 return Optional.ofNullable(reader.getCurrent())
318 }
319
320 /**
321 * @return local db group revision or -1 if not present.
322 */
323 fun getGroupV2Revision(groupId: GroupId.V2): Int {
324 readableDatabase
325 .select()
326 .from(TABLE_NAME)
327 .where("$GROUP_ID = ?", groupId.toString())
328 .run()
329 .use { cursor ->
330 return if (cursor.moveToNext()) {
331 cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION))
332 } else {
333 -1
334 }
335 }
336 }
337
338 fun isUnknownGroup(groupId: GroupId): Boolean {
339 return isUnknownGroup(getGroup(groupId))
340 }
341
342 fun isUnknownGroup(group: Optional<GroupRecord>): Boolean {
343 if (!group.isPresent) {
344 return true
345 }
346
347 val noMetadata = !group.get().hasAvatar() && group.get().title.isNullOrEmpty()
348 val noMembers = group.get().members.isEmpty() || group.get().members.size == 1 && group.get().members.contains(Recipient.self().id)
349
350 return noMetadata && noMembers
351 }
352
353 fun queryGroupsByMemberName(inputQuery: String): Cursor {
354 val subquery = recipients.getAllContactsSubquery(inputQuery)
355 val statement = """
356 SELECT
357 DISTINCT $TABLE_NAME.*,
358 GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}) as $MEMBER_GROUP_CONCAT,
359 ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} as $THREAD_DATE
360 FROM $TABLE_NAME
361 INNER JOIN ${MembershipTable.TABLE_NAME} ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
362 INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID
363 WHERE $TABLE_NAME.$ACTIVE = 1 AND ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} IN (${subquery.where})
364 GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
365 ORDER BY $TITLE COLLATE NOCASE ASC
366 """
367
368 return databaseHelper.signalReadableDatabase.query(statement, subquery.whereArgs)
369 }
370
371 fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
372 val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
373 //language=sql
374 val statement = """
375 $JOINED_GROUP_SELECT
376 WHERE ${query.where}
377 ORDER BY $TITLE COLLATE NOCASE ASC
378 """
379
380 val cursor = databaseHelper.signalReadableDatabase.query(statement, query.whereArgs)
381 return Reader(cursor)
382 }
383
384 fun queryGroupsByMembership(recipientIds: Set<RecipientId>, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
385 var recipientIds = recipientIds
386 if (recipientIds.isEmpty()) {
387 return Reader(null)
388 }
389
390 if (recipientIds.size > 30) {
391 Log.w(TAG, "[queryGroupsByMembership] Large set of recipientIds (${recipientIds.size})! Using the first 30.")
392 recipientIds = recipientIds.take(30).toSet()
393 }
394
395 val membershipQuery = SqlUtil.buildSingleCollectionQuery("${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}", recipientIds)
396
397 var query: String
398 val queryArgs: Array<String>
399
400 if (includeInactive) {
401 query = "${membershipQuery.where} AND ($TABLE_NAME.$ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1))"
402 queryArgs = membershipQuery.whereArgs + buildArgs(1)
403 } else {
404 query = "${membershipQuery.where} AND $TABLE_NAME.$ACTIVE = ?"
405 queryArgs = membershipQuery.whereArgs + buildArgs(1)
406 }
407
408 if (excludeV1) {
409 query += " AND $EXPECTED_V2_ID IS NULL"
410 }
411
412 if (excludeMms) {
413 query += " AND $MMS = 0"
414 }
415
416 val selection = """
417 SELECT DISTINCT
418 $TABLE_NAME.*,
419 (
420 SELECT GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID})
421 FROM ${MembershipTable.TABLE_NAME}
422 WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
423 ) as $MEMBER_GROUP_CONCAT
424 FROM ${MembershipTable.TABLE_NAME}
425 INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
426 WHERE $query
427 """
428
429 return Reader(readableDatabase.query(selection, queryArgs))
430 }
431
432 private fun queryGroupsByRecency(groupQuery: GroupQuery): Reader {
433 val query = getGroupQueryWhereStatement(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
434 val sql = """
435 $JOINED_GROUP_SELECT
436 INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID
437 WHERE ${query.where}
438 ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC
439 """
440
441 return Reader(databaseHelper.signalReadableDatabase.rawQuery(sql, query.whereArgs))
442 }
443
444 fun queryGroups(groupQuery: GroupQuery): Reader {
445 return if (groupQuery.sortOrder === ContactSearchSortOrder.NATURAL) {
446 queryGroupsByTitle(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
447 } else {
448 queryGroupsByRecency(groupQuery)
449 }
450 }
451
452 private fun getGroupQueryWhereStatement(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): SqlUtil.Query {
453 var query: String
454 val queryArgs: Array<String>
455 val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern(inputQuery)
456
457 if (includeInactive) {
458 query = "$TITLE GLOB ? AND ($TABLE_NAME.$ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1))"
459 queryArgs = buildArgs(caseInsensitiveQuery, 1)
460 } else {
461 query = "$TITLE GLOB ? AND $TABLE_NAME.$ACTIVE = ?"
462 queryArgs = buildArgs(caseInsensitiveQuery, 1)
463 }
464
465 if (excludeV1) {
466 query += " AND $EXPECTED_V2_ID IS NULL"
467 }
468
469 if (excludeMms) {
470 query += " AND $MMS = 0"
471 }
472
473 return SqlUtil.Query(query, queryArgs)
474 }
475
476 fun getOrCreateDistributionId(groupId: GroupId.V2): DistributionId {
477 readableDatabase
478 .select(DISTRIBUTION_ID)
479 .from(TABLE_NAME)
480 .where("$GROUP_ID = ?", groupId)
481 .run()
482 .use { cursor ->
483 return if (cursor.moveToFirst()) {
484 val serialized = cursor.optionalString(DISTRIBUTION_ID)
485 if (serialized.isPresent) {
486 DistributionId.from(serialized.get())
487 } else {
488 Log.w(TAG, "Missing distributionId! Creating one.")
489 val distributionId = DistributionId.create()
490
491 val count = writableDatabase
492 .update(TABLE_NAME)
493 .values(DISTRIBUTION_ID to distributionId.toString())
494 .where("$GROUP_ID = ?", groupId)
495 .run()
496
497 check(count >= 1) { "Tried to create a distributionId for $groupId, but it doesn't exist!" }
498
499 distributionId
500 }
501 } else {
502 throw IllegalStateException("Group $groupId doesn't exist!")
503 }
504 }
505 }
506
507 fun getOrCreateMmsGroupForMembers(members: Set<RecipientId>): GroupId.Mms {
508 val joinedTestMembers = members
509 .toList()
510 .map { it.toLong() }
511 .sorted()
512 .joinToString(separator = ",")
513
514 //language=sql
515 val statement = """
516 SELECT
517 $TABLE_NAME.$GROUP_ID as gid,
518 (
519 SELECT GROUP_CONCAT(${MembershipTable.RECIPIENT_ID}, ',')
520 FROM (
521 SELECT ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}
522 FROM ${MembershipTable.TABLE_NAME}
523 WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
524 ORDER BY ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} ASC
525 )
526 ) as $MEMBER_GROUP_CONCAT
527 FROM $TABLE_NAME
528 WHERE $MEMBER_GROUP_CONCAT = ?
529 """
530
531 return readableDatabase.rawQuery(statement, buildArgs(joinedTestMembers)).use { cursor ->
532 if (cursor.moveToNext()) {
533 return GroupId.parseOrThrow(cursor.requireNonNullString("gid")).requireMms()
534 } else {
535 val groupId = GroupId.createMms(SecureRandom())
536 create(groupId, null, members)
537 groupId
538 }
539 }
540 }
541
542 @WorkerThread
543 fun getPushGroupNamesContainingMember(recipientId: RecipientId): List<String> {
544 return getPushGroupsContainingMember(recipientId)
545 .map { groupRecord -> Recipient.resolved(groupRecord.recipientId).getDisplayName(context) }
546 .toList()
547 }
548
549 @WorkerThread
550 fun getPushGroupsContainingMember(recipientId: RecipientId): List<GroupRecord> {
551 return getGroupsContainingMember(recipientId, true)
552 }
553
554 fun getGroupsContainingMember(recipientId: RecipientId, pushOnly: Boolean): List<GroupRecord> {
555 return getGroupsContainingMember(recipientId, pushOnly, false)
556 }
557
558 @WorkerThread
559 fun getGroupsContainingMember(recipientId: RecipientId, pushOnly: Boolean, includeInactive: Boolean): List<GroupRecord> {
560 //language=sql
561 val table = """
562 SELECT
563 DISTINCT $TABLE_NAME.*,
564 (
565 SELECT GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID})
566 FROM ${MembershipTable.TABLE_NAME}
567 WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
568 ) as $MEMBER_GROUP_CONCAT
569 FROM ${MembershipTable.TABLE_NAME}
570 INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
571 LEFT JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
572 """
573
574 var query = "${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} = ?"
575 var args = buildArgs(recipientId)
576 val orderBy = "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC"
577
578 if (pushOnly) {
579 query += " AND $MMS = ?"
580 args = appendArg(args, "0")
581 }
582
583 if (!includeInactive) {
584 query += " AND $TABLE_NAME.$ACTIVE = ?"
585 args = appendArg(args, "1")
586 }
587
588 return readableDatabase
589 .query("$table WHERE $query ORDER BY $orderBy", args)
590 .readToList { cursor ->
591 getGroup(cursor).get()
592 }
593 }
594
595 fun getGroups(): Reader {
596 val cursor = readableDatabase.query(JOINED_GROUP_SELECT)
597 return Reader(cursor)
598 }
599
600 fun getActiveGroupCount(): Int {
601 return readableDatabase
602 .select("COUNT(*)")
603 .from(TABLE_NAME)
604 .where("$ACTIVE = ?", 1)
605 .run()
606 .readToSingleInt(0)
607 }
608
609 @WorkerThread
610 fun getGroupMemberIds(groupId: GroupId, memberSet: MemberSet): List<RecipientId> {
611 return if (groupId.isV2) {
612 getGroup(groupId)
613 .map { it.requireV2GroupProperties().getMemberRecipientIds(memberSet) }
614 .orElse(emptyList())
615 } else {
616 val currentMembers: MutableList<RecipientId> = getCurrentMembers(groupId)
617 if (!memberSet.includeSelf) {
618 currentMembers -= Recipient.self().id
619 }
620 currentMembers
621 }
622 }
623
624 @WorkerThread
625 fun getGroupMembers(groupId: GroupId, memberSet: MemberSet): List<Recipient> {
626 return if (groupId.isV2) {
627 getGroup(groupId)
628 .map { it.requireV2GroupProperties().getMemberRecipients(memberSet) }
629 .orElse(emptyList())
630 } else {
631 val currentMembers: List<RecipientId> = getCurrentMembers(groupId)
632 val recipients: MutableList<Recipient> = ArrayList(currentMembers.size)
633
634 for (member in currentMembers) {
635 val resolved = Recipient.resolved(member)
636 if (memberSet.includeSelf || !resolved.isSelf) {
637 recipients += resolved
638 }
639 }
640
641 recipients
642 }
643 }
644
645 fun getGroupInviter(groupId: GroupId): Recipient? {
646 val groupRecord: Optional<GroupRecord> = getGroup(groupId)
647
648 if (groupRecord.isPresent && groupRecord.get().isV2Group) {
649 val pendingMembers: List<DecryptedPendingMember> = groupRecord.get().requireV2GroupProperties().decryptedGroup.pendingMembers
650 val invitedByAci: ByteString? = DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requireAci())
651 .or { DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requirePni()) }
652 .map { it.addedByAci }
653 .orElse(null)
654
655 if (invitedByAci != null) {
656 val serviceId: ServiceId? = parseOrNull(invitedByAci)
657 if (serviceId != null) {
658 return Recipient.externalPush(serviceId)
659 }
660 }
661 }
662
663 return null
664 }
665
666 @CheckReturnValue
667 fun create(groupId: GroupId.V1, title: String?, members: Collection<RecipientId>, avatar: SignalServiceAttachmentPointer?): Boolean {
668 if (groupExists(groupId.deriveV2MigrationGroupId())) {
669 throw LegacyGroupInsertException(groupId)
670 }
671
672 return create(groupId, title, members, avatar, null, null)
673 }
674
675 @CheckReturnValue
676 fun create(groupId: GroupId.Mms, title: String?, members: Collection<RecipientId>): Boolean {
677 return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null)
678 }
679
680 @JvmOverloads
681 @CheckReturnValue
682 fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup): GroupId.V2? {
683 val groupId = GroupId.v2(groupMasterKey)
684
685 return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState)) {
686 groupId
687 } else {
688 null
689 }
690 }
691
692 /**
693 * There was a point in time where we weren't properly responding to group creates on linked devices. This would result in us having a Recipient entry for the
694 * group, but we'd either be missing the group entry, or that entry would be missing a master key. This method fixes this scenario.
695 */
696 fun fixMissingMasterKey(groupMasterKey: GroupMasterKey) {
697 val groupId = GroupId.v2(groupMasterKey)
698
699 writableDatabase.withinTransaction { db ->
700 val updated = db
701 .update(TABLE_NAME)
702 .values(V2_MASTER_KEY to groupMasterKey.serialize())
703 .where("$GROUP_ID = ?", groupId)
704 .run()
705
706 if (updated < 1) {
707 Log.w(TAG, "No group entry. Creating restore placeholder for $groupId")
708 create(
709 groupMasterKey,
710 DecryptedGroup.Builder()
711 .revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
712 .build()
713 )
714 } else {
715 Log.w(TAG, "Had a group entry, but it was missing a master key. Updated.")
716 }
717 }
718
719 Log.w(TAG, "Scheduling request for latest group info for $groupId")
720 ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(groupId))
721 }
722
723 /**
724 * @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version).
725 */
726 @CheckReturnValue
727 private fun create(
728 groupId: GroupId,
729 title: String?,
730 memberCollection: Collection<RecipientId>,
731 avatar: SignalServiceAttachmentPointer?,
732 groupMasterKey: GroupMasterKey?,
733 groupState: DecryptedGroup?
734 ): Boolean {
735 val membershipValues = mutableListOf<ContentValues>()
736 val groupRecipientId = recipients.getOrInsertFromGroupId(groupId)
737 val members: List<RecipientId> = memberCollection.toSet().sorted()
738 var groupMembers: List<RecipientId> = members
739
740 val values = ContentValues()
741
742 values.put(RECIPIENT_ID, groupRecipientId.serialize())
743 values.put(GROUP_ID, groupId.toString())
744 values.put(TITLE, title)
745 membershipValues.addAll(members.toContentValues(groupId))
746 values.put(MMS, groupId.isMms)
747
748 if (avatar != null) {
749 values.put(AVATAR_ID, avatar.remoteId.v2.get())
750 values.put(AVATAR_KEY, avatar.key)
751 values.put(AVATAR_CONTENT_TYPE, avatar.contentType)
752 values.put(AVATAR_DIGEST, avatar.digest.orElse(null))
753 } else {
754 values.put(AVATAR_ID, 0)
755 }
756
757 values.put(TIMESTAMP, System.currentTimeMillis())
758
759 if (groupId.isV2) {
760 values.put(ACTIVE, if (groupState != null && gv2GroupActive(groupState)) 1 else 0)
761 values.put(DISTRIBUTION_ID, DistributionId.create().toString())
762 } else if (groupId.isV1) {
763 values.put(ACTIVE, 1)
764 values.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString())
765 } else {
766 values.put(ACTIVE, 1)
767 }
768
769 if (groupMasterKey != null) {
770 if (groupState == null) {
771 throw AssertionError("V2 master key but no group state")
772 }
773
774 groupId.requireV2()
775 groupMembers = getV2GroupMembers(groupState, true)
776
777 values.put(V2_MASTER_KEY, groupMasterKey.serialize())
778 values.put(V2_REVISION, groupState.revision)
779 values.put(V2_DECRYPTED_GROUP, groupState.encode())
780 membershipValues.clear()
781 membershipValues.addAll(groupMembers.toContentValues(groupId))
782 } else {
783 if (groupId.isV2) {
784 throw AssertionError("V2 group id but no master key")
785 }
786 }
787
788 writableDatabase.beginTransaction()
789 try {
790 val result: Long = writableDatabase.insert(TABLE_NAME, null, values)
791 if (result < 1) {
792 Log.w(TAG, "Unable to create group, group record already exists")
793 return false
794 }
795
796 for (query in SqlUtil.buildBulkInsert(MembershipTable.TABLE_NAME, arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID), membershipValues)) {
797 writableDatabase.execSQL(query.where, query.whereArgs)
798 }
799 writableDatabase.setTransactionSuccessful()
800 } finally {
801 writableDatabase.endTransaction()
802 }
803
804 if (groupState?.disappearingMessagesTimer != null) {
805 recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer!!.duration)
806 }
807
808 if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {
809 recipients.setHasGroupsInCommon(groupMembers)
810 }
811
812 Recipient.live(groupRecipientId).refresh()
813 notifyConversationListListeners()
814
815 return true
816 }
817
818 fun update(groupId: GroupId.V1, title: String?, avatar: SignalServiceAttachmentPointer?) {
819 val contentValues = ContentValues().apply {
820 if (title != null) {
821 put(TITLE, title)
822 }
823
824 if (avatar != null) {
825 put(AVATAR_ID, avatar.remoteId.v2.get())
826 put(AVATAR_CONTENT_TYPE, avatar.contentType)
827 put(AVATAR_KEY, avatar.key)
828 put(AVATAR_DIGEST, avatar.digest.orElse(null))
829 } else {
830 put(AVATAR_ID, 0)
831 }
832 }
833
834 writableDatabase
835 .update(TABLE_NAME)
836 .values(contentValues)
837 .where("$GROUP_ID = ?", groupId)
838 .run()
839
840 val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
841
842 Recipient.live(groupRecipient).refresh()
843 notifyConversationListListeners()
844 }
845
846 fun update(groupMasterKey: GroupMasterKey, decryptedGroup: DecryptedGroup) {
847 update(GroupId.v2(groupMasterKey), decryptedGroup)
848 }
849
850 fun update(groupId: GroupId.V2, decryptedGroup: DecryptedGroup) {
851 val groupRecipientId: RecipientId = recipients.getOrInsertFromGroupId(groupId)
852 val existingGroup: Optional<GroupRecord> = getGroup(groupId)
853 val title: String = decryptedGroup.title
854
855 val contentValues = ContentValues()
856 contentValues.put(TITLE, title)
857 contentValues.put(V2_REVISION, decryptedGroup.revision)
858 contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.encode())
859 contentValues.put(ACTIVE, if (gv2GroupActive(decryptedGroup)) 1 else 0)
860
861 if (existingGroup.isPresent && existingGroup.get().unmigratedV1Members.isNotEmpty() && existingGroup.get().isV2Group) {
862 val unmigratedV1Members: MutableSet<RecipientId> = existingGroup.get().unmigratedV1Members.toMutableSet()
863
864 val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
865
866 val addedMembers: Set<RecipientId> = change.newMembers.toAciList().toRecipientIds().toSet()
867 val removedMembers: Set<RecipientId> = DecryptedGroupUtil.removedMembersServiceIdList(change).toRecipientIds().toSet()
868 val addedInvites: Set<RecipientId> = DecryptedGroupUtil.pendingToServiceIdList(change.newPendingMembers).toRecipientIds().toSet()
869 val removedInvites: Set<RecipientId> = DecryptedGroupUtil.removedPendingMembersServiceIdList(change).toRecipientIds().toSet()
870 val acceptedInvites: Set<RecipientId> = change.promotePendingMembers.toAciList().toRecipientIds().toSet()
871
872 unmigratedV1Members -= addedMembers
873 unmigratedV1Members -= removedMembers
874 unmigratedV1Members -= addedInvites
875 unmigratedV1Members -= removedInvites
876 unmigratedV1Members -= acceptedInvites
877
878 contentValues.put(UNMIGRATED_V1_MEMBERS, if (unmigratedV1Members.isEmpty()) null else unmigratedV1Members.serialize())
879 }
880
881 val groupMembers = getV2GroupMembers(decryptedGroup, true)
882
883 if (existingGroup.isPresent && existingGroup.get().isV2Group) {
884 val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
885 val removed: List<ServiceId> = DecryptedGroupUtil.removedMembersServiceIdList(change)
886
887 if (removed.isNotEmpty()) {
888 val distributionId = existingGroup.get().distributionId!!
889 Log.i(TAG, removed.size.toString() + " members were removed from group " + groupId + ". Rotating the DistributionId " + distributionId)
890 SenderKeyUtil.rotateOurKey(distributionId)
891 }
892
893 change.promotePendingPniAciMembers.forEach { member ->
894 recipients.getAndPossiblyMergePnpVerified(
895 aci = ACI.parseOrNull(member.aciBytes),
896 pni = PNI.parseOrNull(member.pniBytes),
897 e164 = null
898 )
899 }
900 }
901
902 writableDatabase.withinTransaction { database ->
903 database
904 .update(TABLE_NAME)
905 .values(contentValues)
906 .where("$GROUP_ID = ?", groupId.toString())
907 .run()
908
909 performMembershipUpdate(database, groupId, groupMembers)
910 }
911
912 if (decryptedGroup.disappearingMessagesTimer != null) {
913 recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer!!.duration)
914 }
915
916 if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {
917 recipients.setHasGroupsInCommon(groupMembers)
918 }
919
920 Recipient.live(groupRecipientId).refresh()
921 notifyConversationListListeners()
922 }
923
924 fun updateTitle(groupId: GroupId.V1, title: String?) {
925 updateTitle(groupId as GroupId, title)
926 }
927
928 fun updateTitle(groupId: GroupId.Mms, title: String?) {
929 updateTitle(groupId as GroupId, if (title.isNullOrEmpty()) null else title)
930 }
931
932 private fun updateTitle(groupId: GroupId, title: String?) {
933 if (!groupId.isV1 && !groupId.isMms) {
934 throw AssertionError()
935 }
936
937 writableDatabase
938 .update(TABLE_NAME)
939 .values(TITLE to title)
940 .where("$GROUP_ID = ?", groupId)
941 .run()
942
943 val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
944 Recipient.live(groupRecipient).refresh()
945 }
946
947 /**
948 * Used to bust the Glide cache when an avatar changes.
949 */
950 fun onAvatarUpdated(groupId: GroupId, hasAvatar: Boolean) {
951 writableDatabase
952 .update(TABLE_NAME)
953 .values(AVATAR_ID to if (hasAvatar) Math.abs(SecureRandom().nextLong()) else 0)
954 .where("$GROUP_ID = ?", groupId)
955 .run()
956
957 val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
958 Recipient.live(groupRecipient).refresh()
959 }
960
961 fun updateMembers(groupId: GroupId, members: List<RecipientId>) {
962 writableDatabase.withinTransaction { database ->
963 database
964 .update(TABLE_NAME)
965 .values(ACTIVE to 1)
966 .where("$GROUP_ID = ?", groupId)
967 .run()
968
969 performMembershipUpdate(database, groupId, members)
970 }
971
972 val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
973 Recipient.live(groupRecipient).refresh()
974 }
975
976 fun remove(groupId: GroupId, source: RecipientId) {
977 writableDatabase
978 .delete(MembershipTable.TABLE_NAME)
979 .where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, source)
980 .run()
981
982 val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
983 Recipient.live(groupRecipient).refresh()
984 }
985
986 private fun getCurrentMembers(groupId: GroupId): MutableList<RecipientId> {
987 return readableDatabase
988 .select(MembershipTable.RECIPIENT_ID)
989 .from(MembershipTable.TABLE_NAME)
990 .where("${MembershipTable.GROUP_ID} = ?", groupId)
991 .run()
992 .readToList { cursor ->
993 RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID))
994 }
995 .toMutableList()
996 }
997
998 private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>) {
999 check(database.inTransaction())
1000 database
1001 .delete(MembershipTable.TABLE_NAME)
1002 .where("${MembershipTable.GROUP_ID} = ?", groupId)
1003 .run()
1004
1005 val inserts = SqlUtil.buildBulkInsert(
1006 MembershipTable.TABLE_NAME,
1007 arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID),
1008 members.toContentValues(groupId)
1009 )
1010
1011 inserts.forEach {
1012 database.execSQL(it.where, it.whereArgs)
1013 }
1014 }
1015
1016 fun isActive(groupId: GroupId): Boolean {
1017 val record = getGroup(groupId)
1018 return record.isPresent && record.get().isActive
1019 }
1020
1021 fun setActive(groupId: GroupId, active: Boolean) {
1022 writableDatabase
1023 .update(TABLE_NAME)
1024 .values(ACTIVE to if (active) 1 else 0)
1025 .where("$GROUP_ID = ?", groupId)
1026 .run()
1027 }
1028
1029 fun setLastForceUpdateTimestamp(groupId: GroupId, timestamp: Long) {
1030 writableDatabase
1031 .update(TABLE_NAME)
1032 .values(LAST_FORCE_UPDATE_TIMESTAMP to timestamp)
1033 .where("$GROUP_ID = ?", groupId)
1034 .run()
1035 }
1036
1037 @WorkerThread
1038 fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean {
1039 return readableDatabase
1040 .exists(MembershipTable.TABLE_NAME)
1041 .where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
1042 .run()
1043 }
1044
1045 fun getAllGroupV2Ids(): List<GroupId.V2> {
1046 return readableDatabase
1047 .select(GROUP_ID)
1048 .from(TABLE_NAME)
1049 .run()
1050 .readToList { GroupId.parseOrThrow(it.requireNonNullString(GROUP_ID)) }
1051 .filter { it.isV2 }
1052 .map { it.requireV2() }
1053 }
1054
1055 /**
1056 * Key: The 'expected' V2 ID (i.e. what a V1 ID would map to when migrated)
1057 * Value: The matching V1 ID
1058 */
1059 fun getAllExpectedV2Ids(): Map<GroupId.V2, GroupId.V1> {
1060 return readableDatabase
1061 .select(GROUP_ID, EXPECTED_V2_ID)
1062 .from(TABLE_NAME)
1063 .where("$EXPECTED_V2_ID NOT NULL")
1064 .run()
1065 .readToList { cursor ->
1066 val groupId = GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)).requireV1()
1067 val expectedId = GroupId.parseOrThrow(cursor.requireNonNullString(EXPECTED_V2_ID)).requireV2()
1068 expectedId to groupId
1069 }
1070 .toMap()
1071 }
1072
1073 override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
1074 // Remap all recipients that would not result in conflicts
1075 writableDatabase.execSQL(
1076 """
1077 UPDATE ${MembershipTable.TABLE_NAME} AS parent
1078 SET ${MembershipTable.RECIPIENT_ID} = ?
1079 WHERE
1080 ${MembershipTable.RECIPIENT_ID} = ?
1081 AND NOT EXISTS (
1082 SELECT 1
1083 FROM ${MembershipTable.TABLE_NAME} child
1084 WHERE
1085 child.${MembershipTable.RECIPIENT_ID} = ?
1086 AND parent.${MembershipTable.GROUP_ID} = child.${MembershipTable.GROUP_ID}
1087 )
1088 """,
1089 buildArgs(toId, fromId, toId)
1090 )
1091
1092 // Delete the remaining fromId's (the only remaining ones should be those in groups where the toId is already present)
1093 writableDatabase
1094 .delete(MembershipTable.TABLE_NAME)
1095 .where("${MembershipTable.RECIPIENT_ID} = ?", fromId)
1096 .run()
1097
1098 for (group in getGroupsContainingMember(fromId, pushOnly = false, includeInactive = true)) {
1099 if (group.isV2Group) {
1100 removeUnmigratedV1Members(group.id.requireV2(), listOf(fromId))
1101 }
1102 }
1103 }
1104
1105 class Reader(val cursor: Cursor?) : Closeable, ContactSearchIterator<GroupRecord> {
1106
1107 fun getNext(): GroupRecord? {
1108 return if (cursor == null || !cursor.moveToNext()) {
1109 null
1110 } else {
1111 getCurrent()
1112 }
1113 }
1114
1115 override fun getCount(): Int {
1116 return cursor?.count ?: 0
1117 }
1118
1119 fun getCurrent(): GroupRecord? {
1120 return if (cursor == null || cursor.requireString(GROUP_ID) == null || cursor.requireLong(RECIPIENT_ID) == 0L) {
1121 null
1122 } else {
1123 GroupRecord(
1124 id = GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)),
1125 recipientId = RecipientId.from(cursor.requireNonNullString(RECIPIENT_ID)),
1126 title = cursor.requireString(TITLE),
1127 serializedMembers = cursor.requireString(MEMBER_GROUP_CONCAT),
1128 serializedUnmigratedV1Members = cursor.requireString(UNMIGRATED_V1_MEMBERS),
1129 avatarId = cursor.requireLong(AVATAR_ID),
1130 avatarKey = cursor.requireBlob(AVATAR_KEY),
1131 avatarContentType = cursor.requireString(AVATAR_CONTENT_TYPE),
1132 isActive = cursor.requireBoolean(ACTIVE),
1133 avatarDigest = cursor.requireBlob(AVATAR_DIGEST),
1134 isMms = cursor.requireBoolean(MMS),
1135 groupMasterKeyBytes = cursor.requireBlob(V2_MASTER_KEY),
1136 groupRevision = cursor.requireInt(V2_REVISION),
1137 decryptedGroupBytes = cursor.requireBlob(V2_DECRYPTED_GROUP),
1138 distributionId = cursor.optionalString(DISTRIBUTION_ID).map { id -> DistributionId.from(id) }.orElse(null),
1139 lastForceUpdateTimestamp = cursor.requireLong(LAST_FORCE_UPDATE_TIMESTAMP)
1140 )
1141 }
1142 }
1143
1144 override fun close() {
1145 cursor?.close()
1146 }
1147
1148 override fun moveToPosition(n: Int) {
1149 cursor!!.moveToPosition(n)
1150 }
1151
1152 override fun hasNext(): Boolean {
1153 return cursor != null && !cursor.isLast && !cursor.isAfterLast
1154 }
1155
1156 override fun next(): GroupRecord {
1157 return getNext()!!
1158 }
1159 }
1160
1161 class V2GroupProperties(val groupMasterKey: GroupMasterKey, val groupRevision: Int, val decryptedGroupBytes: ByteArray) {
1162 val decryptedGroup: DecryptedGroup by lazy {
1163 DecryptedGroup.ADAPTER.decode(decryptedGroupBytes)
1164 }
1165
1166 val bannedMembers: Set<ServiceId> by lazy {
1167 DecryptedGroupUtil.bannedMembersToServiceIdSet(decryptedGroup.bannedMembers)
1168 }
1169
1170 fun isAdmin(recipient: Recipient): Boolean {
1171 val aci = recipient.aci
1172
1173 return if (aci.isPresent) {
1174 decryptedGroup.members.findMemberByAci(aci.get())
1175 .map { it.role == Member.Role.ADMINISTRATOR }
1176 .orElse(false)
1177 } else {
1178 false
1179 }
1180 }
1181
1182 fun getAdmins(members: List<Recipient>): List<Recipient> {
1183 return members.stream().filter { recipient: Recipient -> isAdmin(recipient) }.collect(Collectors.toList())
1184 }
1185
1186 fun memberLevel(serviceIdOptional: Optional<ServiceId>): MemberLevel {
1187 if (serviceIdOptional.isEmpty) {
1188 return MemberLevel.NOT_A_MEMBER
1189 }
1190
1191 val serviceId: ServiceId = serviceIdOptional.get()
1192 var memberLevel: Optional<MemberLevel> = Optional.empty()
1193
1194 if (serviceId is ACI) {
1195 memberLevel = decryptedGroup.members.findMemberByAci(serviceId)
1196 .map { member ->
1197 if (member.role == Member.Role.ADMINISTRATOR) {
1198 MemberLevel.ADMINISTRATOR
1199 } else {
1200 MemberLevel.FULL_MEMBER
1201 }
1202 }
1203 }
1204
1205 if (memberLevel.isAbsent()) {
1206 memberLevel = decryptedGroup.pendingMembers.findPendingByServiceId(serviceId)
1207 .map { MemberLevel.PENDING_MEMBER }
1208 }
1209
1210 if (memberLevel.isAbsent() && serviceId is ACI) {
1211 memberLevel = decryptedGroup.requestingMembers.findRequestingByAci(serviceId)
1212 .map { _ -> MemberLevel.REQUESTING_MEMBER }
1213 }
1214
1215 return if (memberLevel.isPresent) {
1216 memberLevel.get()
1217 } else {
1218 MemberLevel.NOT_A_MEMBER
1219 }
1220 }
1221
1222 fun getMemberRecipients(memberSet: MemberSet): List<Recipient> {
1223 return Recipient.resolvedList(getMemberRecipientIds(memberSet))
1224 }
1225
1226 fun getMemberRecipientIds(memberSet: MemberSet): List<RecipientId> {
1227 val includeSelf = memberSet.includeSelf
1228 val selfAci = SignalStore.account().requireAci()
1229 val recipients: MutableList<RecipientId> = ArrayList(decryptedGroup.members.size + decryptedGroup.pendingMembers.size)
1230
1231 var unknownMembers = 0
1232 var unknownPending = 0
1233
1234 for (aci in decryptedGroup.members.toAciListWithUnknowns()) {
1235 if (aci.isUnknown) {
1236 unknownMembers++
1237 } else if (includeSelf || selfAci != aci) {
1238 recipients += RecipientId.from(aci)
1239 }
1240 }
1241
1242 if (memberSet.includePending) {
1243 for (serviceId in DecryptedGroupUtil.pendingToServiceIdList(decryptedGroup.pendingMembers)) {
1244 if (serviceId.isUnknown) {
1245 unknownPending++
1246 } else if (includeSelf || selfAci != serviceId) {
1247 recipients += RecipientId.from(serviceId)
1248 }
1249 }
1250 }
1251
1252 if (unknownMembers + unknownPending > 0) {
1253 Log.w(TAG, "Group contains $unknownPending unknown pending and $unknownMembers unknown full members")
1254 }
1255
1256 return recipients
1257 }
1258
1259 fun getMemberServiceIds(): List<ServiceId> {
1260 return decryptedGroup
1261 .members
1262 .asSequence()
1263 .map { ACI.parseOrNull(it.aciBytes) }
1264 .filterNotNull()
1265 .sortedBy { it.toString() }
1266 .toList()
1267 }
1268 }
1269
1270 @Throws(BadGroupIdException::class)
1271 fun getGroupsToDisplayAsStories(): List<GroupId> {
1272 @Language("sql")
1273 val query = """
1274 SELECT
1275 $GROUP_ID,
1276 (
1277 SELECT ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED}
1278 FROM ${MessageTable.TABLE_NAME}
1279 WHERE
1280 ${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AND
1281 ${MessageTable.STORY_TYPE} > 1
1282 ORDER BY ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} DESC
1283 LIMIT 1
1284 ) AS active_timestamp
1285 FROM $TABLE_NAME INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID
1286 WHERE
1287 $TABLE_NAME.$ACTIVE = 1 AND
1288 (
1289 $SHOW_AS_STORY_STATE = ${ShowAsStoryState.ALWAYS.code} OR
1290 (
1291 $SHOW_AS_STORY_STATE = ${ShowAsStoryState.IF_ACTIVE.code} AND
1292 active_timestamp IS NOT NULL
1293 )
1294 )
1295 ORDER BY active_timestamp DESC
1296 """
1297
1298 return readableDatabase
1299 .query(query)
1300 .readToList { cursor ->
1301 GroupId.parse(cursor.requireNonNullString(GROUP_ID))
1302 }
1303 }
1304
1305 fun getShowAsStoryState(groupId: GroupId): ShowAsStoryState {
1306 return readableDatabase
1307 .select(SHOW_AS_STORY_STATE)
1308 .from(TABLE_NAME)
1309 .where("$GROUP_ID = ?", groupId)
1310 .run()
1311 .readToSingleObject { cursor ->
1312 val serializedState = cursor.requireInt(SHOW_AS_STORY_STATE)
1313 ShowAsStoryState.deserialize(serializedState)
1314 } ?: throw AssertionError("Group $groupId does not exist!")
1315 }
1316
1317 fun setShowAsStoryState(groupId: GroupId, showAsStoryState: ShowAsStoryState) {
1318 writableDatabase
1319 .update(TABLE_NAME)
1320 .values(SHOW_AS_STORY_STATE to showAsStoryState.code)
1321 .where("$GROUP_ID = ?", groupId)
1322 .run()
1323 }
1324
1325 fun setShowAsStoryState(recipientIds: Collection<RecipientId?>, showAsStoryState: ShowAsStoryState) {
1326 val queries = buildCollectionQuery(RECIPIENT_ID, recipientIds)
1327 val contentValues = contentValuesOf(SHOW_AS_STORY_STATE to showAsStoryState.code)
1328
1329 writableDatabase.withinTransaction { db ->
1330 for (query in queries) {
1331 db.update(TABLE_NAME, contentValues, query.where, query.whereArgs)
1332 }
1333 }
1334 }
1335
1336 private fun gv2GroupActive(decryptedGroup: DecryptedGroup): Boolean {
1337 val aci = SignalStore.account().requireAci()
1338
1339 return decryptedGroup.members.findMemberByAci(aci).isPresent ||
1340 DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, aci).isPresent
1341 }
1342
1343 private fun List<ServiceId>.toRecipientIds(): MutableList<RecipientId> {
1344 return serviceIdsToRecipientIds(this.asSequence())
1345 }
1346
1347 private fun Collection<RecipientId>.serialize(): String {
1348 return RecipientId.toSerializedList(this)
1349 }
1350
1351 private fun Collection<RecipientId>.toContentValues(groupId: GroupId): List<ContentValues> {
1352 return map {
1353 contentValuesOf(
1354 MembershipTable.GROUP_ID to groupId.serialize(),
1355 MembershipTable.RECIPIENT_ID to it.serialize()
1356 )
1357 }
1358 }
1359
1360 private fun serviceIdsToRecipientIds(serviceIds: Sequence<ServiceId>): MutableList<RecipientId> {
1361 return serviceIds
1362 .map { serviceId ->
1363 if (serviceId.isUnknown) {
1364 Log.w(TAG, "Saw an unknown UUID when mapping to RecipientIds!")
1365 null
1366 } else {
1367 val id = RecipientId.from(serviceId)
1368 val remapped = RemappedRecords.getInstance().getRecipient(id)
1369 if (remapped.isPresent) {
1370 Log.w(TAG, "Saw that $id remapped to $remapped. Using the mapping.")
1371 remapped.get()
1372 } else {
1373 id
1374 }
1375 }
1376 }
1377 .filterNotNull()
1378 .sorted()
1379 .toMutableList()
1380 }
1381
1382 private fun getV2GroupMembers(decryptedGroup: DecryptedGroup, shouldRetry: Boolean): List<RecipientId> {
1383 val ids: List<RecipientId> = decryptedGroup.members.toAciList().toRecipientIds()
1384
1385 return if (RemappedRecords.getInstance().areAnyRemapped(ids)) {
1386 if (shouldRetry) {
1387 Log.w(TAG, "Found remapped records where we shouldn't. Clearing cache and trying again.")
1388 RecipientId.clearCache()
1389 RemappedRecords.getInstance().resetCache()
1390 getV2GroupMembers(decryptedGroup, false)
1391 } else {
1392 throw IllegalStateException("Remapped records in group membership!")
1393 }
1394 } else {
1395 ids
1396 }
1397 }
1398
1399 enum class MemberSet(val includeSelf: Boolean, val includePending: Boolean) {
1400 FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false), FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true)
1401 }
1402
1403 /**
1404 * State object describing whether or not to display a story in a list.
1405 */
1406 enum class ShowAsStoryState(val code: Int) {
1407 /**
1408 * The default value. Display the group as a story if the group has stories in it currently.
1409 */
1410 IF_ACTIVE(0),
1411
1412 /**
1413 * Always display the group as a story unless explicitly removed. This state is entered if the
1414 * user sends a story to a group or otherwise explicitly selects it to appear.
1415 */
1416 ALWAYS(1),
1417
1418 /**
1419 * Never display the story as a group. This state is entered if the user removes the group from
1420 * their list, and is only navigated away from if the user explicitly adds the group again.
1421 */
1422 NEVER(2);
1423
1424 companion object {
1425 fun deserialize(code: Int): ShowAsStoryState {
1426 return when (code) {
1427 0 -> IF_ACTIVE
1428 1 -> ALWAYS
1429 2 -> NEVER
1430 else -> throw IllegalArgumentException("Unknown code: $code")
1431 }
1432 }
1433 }
1434 }
1435
1436 enum class MemberLevel(val isInGroup: Boolean) {
1437 NOT_A_MEMBER(false),
1438 PENDING_MEMBER(false),
1439 REQUESTING_MEMBER(false),
1440 FULL_MEMBER(true),
1441 ADMINISTRATOR(true)
1442 }
1443
1444 class GroupQuery private constructor(builder: Builder) {
1445 val searchQuery: String
1446 val includeInactive: Boolean
1447 val includeV1: Boolean
1448 val includeMms: Boolean
1449 val sortOrder: ContactSearchSortOrder
1450
1451 init {
1452 searchQuery = builder.searchQuery
1453 includeInactive = builder.includeInactive
1454 includeV1 = builder.includeV1
1455 includeMms = builder.includeMms
1456 sortOrder = builder.sortOrder
1457 }
1458
1459 class Builder {
1460 var searchQuery = ""
1461 var includeInactive = false
1462 var includeV1 = false
1463 var includeMms = false
1464 var sortOrder = ContactSearchSortOrder.NATURAL
1465 fun withSearchQuery(query: String?): Builder {
1466 searchQuery = if (TextUtils.isEmpty(query)) "" else query!!
1467 return this
1468 }
1469
1470 fun withInactiveGroups(includeInactive: Boolean): Builder {
1471 this.includeInactive = includeInactive
1472 return this
1473 }
1474
1475 fun withV1Groups(includeV1Groups: Boolean): Builder {
1476 includeV1 = includeV1Groups
1477 return this
1478 }
1479
1480 fun withMmsGroups(includeMmsGroups: Boolean): Builder {
1481 includeMms = includeMmsGroups
1482 return this
1483 }
1484
1485 fun withSortOrder(sortOrder: ContactSearchSortOrder): Builder {
1486 this.sortOrder = sortOrder
1487 return this
1488 }
1489
1490 fun build(): GroupQuery {
1491 return GroupQuery(this)
1492 }
1493 }
1494 }
1495
1496 class LegacyGroupInsertException(id: GroupId?) : IllegalStateException("Tried to create a new GV1 entry when we already had a migrated GV2! $id")
1497}