That fuck shit the fascists are using
at master 1497 lines 53 kB view raw
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}