That fuck shit the fascists are using
at master 546 lines 26 kB view raw
1package org.tm.archive.groups; 2 3import android.content.Context; 4 5import androidx.annotation.NonNull; 6import androidx.annotation.Nullable; 7import androidx.annotation.WorkerThread; 8 9import org.signal.core.util.logging.Log; 10import org.signal.libsignal.zkgroup.VerificationFailedException; 11import org.signal.libsignal.zkgroup.groups.GroupMasterKey; 12import org.signal.libsignal.zkgroup.groups.GroupSecretParams; 13import org.signal.libsignal.zkgroup.groups.UuidCiphertext; 14import org.signal.storageservice.protos.groups.GroupExternalCredential; 15import org.signal.storageservice.protos.groups.local.DecryptedGroup; 16import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; 17import org.tm.archive.database.GroupTable; 18import org.tm.archive.database.SignalDatabase; 19import org.tm.archive.database.model.GroupRecord; 20import org.tm.archive.groups.v2.GroupInviteLinkUrl; 21import org.tm.archive.groups.v2.GroupLinkPassword; 22import org.tm.archive.groups.v2.processing.GroupsV2StateProcessor; 23import org.tm.archive.profiles.AvatarHelper; 24import org.tm.archive.recipients.Recipient; 25import org.tm.archive.recipients.RecipientId; 26import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; 27import org.whispersystems.signalservice.api.push.ServiceId; 28 29import java.io.IOException; 30import java.util.Collection; 31import java.util.Collections; 32import java.util.HashSet; 33import java.util.List; 34import java.util.Map; 35import java.util.Optional; 36import java.util.Set; 37import java.util.UUID; 38 39public final class GroupManager { 40 41 private static final String TAG = Log.tag(GroupManager.class); 42 43 @WorkerThread 44 public static @NonNull GroupActionResult createGroup(@NonNull ServiceId authServiceId, 45 @NonNull Context context, 46 @NonNull Set<Recipient> members, 47 @Nullable byte[] avatar, 48 @Nullable String name, 49 boolean mms, 50 int disappearingMessagesTimer) 51 throws GroupChangeBusyException, GroupChangeFailedException, IOException 52 { 53 boolean shouldAttemptToCreateV2 = !mms; 54 Set<RecipientId> memberIds = getMemberIds(members); 55 56 if (shouldAttemptToCreateV2) { 57 try { 58 try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) { 59 return groupCreator.createGroup(authServiceId, memberIds, name, avatar, disappearingMessagesTimer); 60 } 61 } catch (MembershipNotSuitableForV2Exception e) { 62 Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e); 63 64 return GroupManagerV1.createGroup(context, memberIds, avatar, name, false); 65 } 66 } else { 67 return GroupManagerV1.createGroup(context, memberIds, avatar, name, mms); 68 } 69 } 70 71 @WorkerThread 72 public static GroupActionResult updateGroupDetails(@NonNull Context context, 73 @NonNull GroupId groupId, 74 @Nullable byte[] avatar, 75 boolean avatarChanged, 76 @NonNull String name, 77 boolean nameChanged, 78 @NonNull String description, 79 boolean descriptionChanged) 80 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 81 { 82 if (groupId.isV2()) { 83 try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { 84 return edit.updateGroupTitleDescriptionAndAvatar(nameChanged ? name : null, 85 descriptionChanged ? description : null, 86 avatar, 87 avatarChanged); 88 } 89 } else if (groupId.isV1()) { 90 List<Recipient> members = SignalDatabase.groups() 91 .getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); 92 93 Set<RecipientId> recipientIds = getMemberIds(new HashSet<>(members)); 94 95 return GroupManagerV1.updateGroup(context, groupId.requireV1(), recipientIds, avatar, name, 0); 96 } else { 97 return GroupManagerV1.updateGroup(context, groupId.requireMms(), avatar, name); 98 } 99 } 100 101 @WorkerThread 102 public static void migrateGroupToServer(@NonNull Context context, 103 @NonNull GroupId.V1 groupIdV1, 104 @NonNull Collection<Recipient> members) 105 throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException 106 { 107 new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1, members); 108 } 109 110 private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) { 111 Set<RecipientId> results = new HashSet<>(recipients.size()); 112 113 for (Recipient recipient : recipients) { 114 results.add(recipient.getId()); 115 } 116 117 return results; 118 } 119 120 @WorkerThread 121 public static void leaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId, boolean sendToMembers) 122 throws GroupChangeBusyException, GroupChangeFailedException, IOException 123 { 124 if (groupId.isV2()) { 125 try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { 126 edit.leaveGroup(sendToMembers); 127 Log.i(TAG, "Left group " + groupId); 128 } catch (GroupInsufficientRightsException e) { 129 Log.w(TAG, "Unexpected prevention from leaving " + groupId + " due to rights", e); 130 throw new GroupChangeFailedException(e); 131 } catch (GroupNotAMemberException e) { 132 Log.w(TAG, "Already left group " + groupId, e); 133 } 134 } else { 135 if (!GroupManagerV1.leaveGroup(context, groupId.requireV1())) { 136 Log.w(TAG, "GV1 group leave failed" + groupId); 137 throw new GroupChangeFailedException(); 138 } 139 } 140 141 SignalDatabase.recipients().getByGroupId(groupId).ifPresent(id -> SignalDatabase.messages().deleteScheduledMessages(id)); 142 } 143 144 @WorkerThread 145 public static void leaveGroupFromBlockOrMessageRequest(@NonNull Context context, @NonNull GroupId.Push groupId) 146 throws IOException, GroupChangeBusyException, GroupChangeFailedException 147 { 148 if (groupId.isV2()) { 149 leaveGroup(context, groupId.requireV2(), true); 150 } else { 151 if (!GroupManagerV1.silentLeaveGroup(context, groupId.requireV1())) { 152 throw new GroupChangeFailedException(); 153 } 154 } 155 } 156 157 @WorkerThread 158 public static void addMemberAdminsAndLeaveGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Collection<RecipientId> newAdmins) 159 throws GroupChangeBusyException, GroupChangeFailedException, IOException, GroupInsufficientRightsException, GroupNotAMemberException 160 { 161 try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { 162 edit.addMemberAdminsAndLeaveGroup(newAdmins); 163 Log.i(TAG, "Left group " + groupId); 164 } 165 } 166 167 @WorkerThread 168 public static void ejectAndBanFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient) 169 throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException 170 { 171 try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) { 172 edit.ejectMember(recipient.requireAci(), false, true, true); 173 Log.i(TAG, "Member removed from group " + groupId); 174 } 175 } 176 177 /** 178 * @throws GroupNotAMemberException When Self is not a member of the group. 179 * The exception to this is when Self is a requesting member and 180 * there is a supplied signedGroupChange. This allows for 181 * processing deny messages. 182 */ 183 @WorkerThread 184 public static GroupsV2StateProcessor.GroupUpdateResult updateGroupFromServer(@NonNull Context context, 185 @NonNull GroupMasterKey groupMasterKey, 186 int revision, 187 long timestamp, 188 @Nullable byte[] signedGroupChange) 189 throws GroupChangeBusyException, IOException, GroupNotAMemberException 190 { 191 try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { 192 return updater.updateLocalToServerRevision(revision, timestamp, null, signedGroupChange); 193 } 194 } 195 196 @WorkerThread 197 public static GroupsV2StateProcessor.GroupUpdateResult updateGroupFromServer(@NonNull Context context, 198 @NonNull GroupMasterKey groupMasterKey, 199 @NonNull Optional<GroupRecord> groupRecord, 200 @Nullable GroupSecretParams groupSecretParams, 201 int revision, 202 long timestamp, 203 @Nullable byte[] signedGroupChange, 204 @Nullable String serverGuid) 205 throws GroupChangeBusyException, IOException, GroupNotAMemberException 206 { 207 try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { 208 return updater.updateLocalToServerRevision(revision, timestamp, groupRecord, groupSecretParams, signedGroupChange, serverGuid); 209 } 210 } 211 212 @WorkerThread 213 public static void forceSanityUpdateFromServer(@NonNull Context context, 214 @NonNull GroupMasterKey groupMasterKey, 215 long timestamp) 216 throws GroupChangeBusyException, IOException, GroupNotAMemberException 217 { 218 try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { 219 updater.forceSanityUpdateFromServer(timestamp); 220 } 221 } 222 223 @WorkerThread 224 public static V2GroupServerStatus v2GroupStatus(@NonNull Context context, 225 @NonNull ServiceId authServiceId, 226 @NonNull GroupMasterKey groupMasterKey) 227 throws IOException 228 { 229 try { 230 new GroupManagerV2(context).groupServerQuery(authServiceId, groupMasterKey); 231 return V2GroupServerStatus.FULL_OR_PENDING_MEMBER; 232 } catch (GroupNotAMemberException e) { 233 return V2GroupServerStatus.NOT_A_MEMBER; 234 } catch (GroupDoesNotExistException e) { 235 return V2GroupServerStatus.DOES_NOT_EXIST; 236 } 237 } 238 239 /** 240 * Tries to gets the exact version of the group at the time you joined. 241 * <p> 242 * If it fails to get the exact version, it will give the latest. 243 */ 244 @WorkerThread 245 public static DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, 246 @NonNull Context context, 247 @NonNull GroupMasterKey groupMasterKey) 248 throws IOException, GroupDoesNotExistException, GroupNotAMemberException 249 { 250 return new GroupManagerV2(context).addedGroupVersion(authServiceId, groupMasterKey); 251 } 252 253 @WorkerThread 254 public static void setMemberAdmin(@NonNull Context context, 255 @NonNull GroupId.V2 groupId, 256 @NonNull RecipientId recipientId, 257 boolean admin) 258 throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException 259 { 260 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 261 editor.setMemberAdmin(recipientId, admin); 262 } 263 } 264 265 @WorkerThread 266 public static void updateSelfProfileKeyInGroup(@NonNull Context context, @NonNull GroupId.V2 groupId) 267 throws IOException, GroupChangeBusyException, GroupInsufficientRightsException, GroupNotAMemberException, GroupChangeFailedException 268 { 269 if (!SignalDatabase.groups().groupExists(groupId)) { 270 Log.i(TAG, "Group is not available locally " + groupId); 271 return; 272 } 273 274 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 275 editor.updateSelfProfileKeyInGroup(); 276 } 277 } 278 279 @WorkerThread 280 public static void acceptInvite(@NonNull Context context, @NonNull GroupId.V2 groupId) 281 throws GroupChangeBusyException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException 282 { 283 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 284 editor.acceptInvite(); 285 SignalDatabase.groups().setActive(groupId, true); 286 } 287 } 288 289 @WorkerThread 290 public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime) 291 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 292 { 293 if (groupId.isV2()) { 294 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 295 editor.updateGroupTimer(expirationTime); 296 } 297 } else { 298 GroupManagerV1.updateGroupTimer(context, groupId.requireV1(), expirationTime); 299 } 300 } 301 302 @WorkerThread 303 public static void revokeInvites(@NonNull Context context, 304 @NonNull ServiceId authServiceId, 305 @NonNull GroupId.V2 groupId, 306 @NonNull Collection<UuidCiphertext> uuidCipherTexts) 307 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 308 { 309 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 310 editor.revokeInvites(authServiceId, uuidCipherTexts, true); 311 } 312 } 313 314 @WorkerThread 315 public static void ban(@NonNull Context context, 316 @NonNull GroupId.V2 groupId, 317 @NonNull RecipientId recipientId) 318 throws GroupChangeBusyException, IOException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException 319 { 320 GroupTable.V2GroupProperties groupProperties = SignalDatabase.groups().requireGroup(groupId).requireV2GroupProperties(); 321 Recipient recipient = Recipient.resolved(recipientId); 322 323 if (groupProperties.getBannedMembers().contains(recipient.requireServiceId())) { 324 Log.i(TAG, "Attempt to ban already banned recipient: " + recipientId); 325 return; 326 } 327 328 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 329 editor.ban(recipient.requireServiceId()); 330 } 331 } 332 333 @WorkerThread 334 public static void unban(@NonNull Context context, 335 @NonNull GroupId.V2 groupId, 336 @NonNull RecipientId recipientId) 337 throws GroupChangeBusyException, IOException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException 338 { 339 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 340 editor.unban(Collections.singleton(Recipient.resolved(recipientId).requireServiceId())); 341 } 342 } 343 344 @WorkerThread 345 public static void applyMembershipAdditionRightsChange(@NonNull Context context, 346 @NonNull GroupId.V2 groupId, 347 @NonNull GroupAccessControl newRights) 348 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 349 { 350 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 351 editor.updateMembershipRights(newRights); 352 } 353 } 354 355 @WorkerThread 356 public static void applyAttributesRightsChange(@NonNull Context context, 357 @NonNull GroupId.V2 groupId, 358 @NonNull GroupAccessControl newRights) 359 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 360 { 361 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 362 editor.updateAttributesRights(newRights); 363 } 364 } 365 366 @WorkerThread 367 public static void applyAnnouncementGroupChange(@NonNull Context context, 368 @NonNull GroupId.V2 groupId, 369 @NonNull boolean isAnnouncementGroup) 370 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 371 { 372 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 373 editor.updateAnnouncementGroup(isAnnouncementGroup); 374 } 375 } 376 377 @WorkerThread 378 public static void cycleGroupLinkPassword(@NonNull Context context, 379 @NonNull GroupId.V2 groupId) 380 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 381 { 382 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 383 editor.cycleGroupLinkPassword(); 384 } 385 } 386 387 @WorkerThread 388 public static GroupInviteLinkUrl setGroupLinkEnabledState(@NonNull Context context, 389 @NonNull GroupId.V2 groupId, 390 @NonNull GroupLinkState state) 391 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 392 { 393 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 394 return editor.setJoinByGroupLinkState(state); 395 } 396 } 397 398 @WorkerThread 399 public static void approveRequests(@NonNull Context context, 400 @NonNull GroupId.V2 groupId, 401 @NonNull Collection<RecipientId> recipientIds) 402 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 403 { 404 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 405 editor.approveRequests(recipientIds); 406 } 407 } 408 409 @WorkerThread 410 public static void denyRequests(@NonNull Context context, 411 @NonNull GroupId.V2 groupId, 412 @NonNull Collection<RecipientId> recipientIds) 413 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException 414 { 415 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 416 editor.denyRequests(recipientIds); 417 } 418 } 419 420 @WorkerThread 421 public static @NonNull GroupActionResult addMembers(@NonNull Context context, 422 @NonNull GroupId.Push groupId, 423 @NonNull Collection<RecipientId> newMembers) 424 throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException, MembershipNotSuitableForV2Exception 425 { 426 if (groupId.isV2()) { 427 GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId); 428 429 try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { 430 return editor.addMembers(newMembers, groupRecord.requireV2GroupProperties().getBannedMembers()); 431 } 432 } else { 433 GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId); 434 List<RecipientId> members = groupRecord.getMembers(); 435 byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null; 436 Set<RecipientId> recipientIds = new HashSet<>(members); 437 int originalSize = recipientIds.size(); 438 439 recipientIds.addAll(newMembers); 440 return GroupManagerV1.updateGroup(context, groupId, recipientIds, avatar, groupRecord.getTitle(), recipientIds.size() - originalSize); 441 } 442 } 443 444 /** 445 * Use to get a group's details direct from server bypassing the database. 446 * <p> 447 * Useful when you don't yet have the group in the database locally. 448 */ 449 @WorkerThread 450 public static @NonNull DecryptedGroupJoinInfo getGroupJoinInfoFromServer(@NonNull Context context, 451 @NonNull GroupMasterKey groupMasterKey, 452 @Nullable GroupLinkPassword groupLinkPassword) 453 throws IOException, VerificationFailedException, GroupLinkNotActiveException 454 { 455 return new GroupManagerV2(context).getGroupJoinInfoFromServer(groupMasterKey, groupLinkPassword); 456 } 457 458 @WorkerThread 459 public static GroupActionResult joinGroup(@NonNull Context context, 460 @NonNull GroupMasterKey groupMasterKey, 461 @NonNull GroupLinkPassword groupLinkPassword, 462 @NonNull DecryptedGroupJoinInfo decryptedGroupJoinInfo, 463 @Nullable byte[] avatar) 464 throws IOException, GroupChangeBusyException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException 465 { 466 try (GroupManagerV2.GroupJoiner join = new GroupManagerV2(context).join(groupMasterKey, groupLinkPassword)) { 467 return join.joinGroup(decryptedGroupJoinInfo, avatar); 468 } 469 } 470 471 @WorkerThread 472 public static void cancelJoinRequest(@NonNull Context context, 473 @NonNull GroupId.V2 groupId) 474 throws GroupChangeFailedException, IOException, GroupChangeBusyException 475 { 476 try (GroupManagerV2.GroupJoiner editor = new GroupManagerV2(context).cancelRequest(groupId.requireV2())) { 477 editor.cancelJoinRequest(); 478 } 479 } 480 481 public static void sendNoopUpdate(@NonNull Context context, @NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup currentState) { 482 new GroupManagerV2(context).sendNoopGroupUpdate(groupMasterKey, currentState); 483 } 484 485 @WorkerThread 486 public static @NonNull GroupExternalCredential getGroupExternalCredential(@NonNull Context context, 487 @NonNull GroupId.V2 groupId) 488 throws IOException, VerificationFailedException 489 { 490 return new GroupManagerV2(context).getGroupExternalCredential(groupId); 491 } 492 493 @WorkerThread 494 public static @NonNull Map<UUID, UuidCiphertext> getUuidCipherTexts(@NonNull Context context, @NonNull GroupId.V2 groupId) { 495 return new GroupManagerV2(context).getUuidCipherTexts(groupId); 496 } 497 498 public static class GroupActionResult { 499 private final Recipient groupRecipient; 500 private final long threadId; 501 private final int addedMemberCount; 502 private final List<RecipientId> invitedMembers; 503 504 public GroupActionResult(@NonNull Recipient groupRecipient, 505 long threadId, 506 int addedMemberCount, 507 @NonNull List<RecipientId> invitedMembers) 508 { 509 this.groupRecipient = groupRecipient; 510 this.threadId = threadId; 511 this.addedMemberCount = addedMemberCount; 512 this.invitedMembers = invitedMembers; 513 } 514 515 public @NonNull Recipient getGroupRecipient() { 516 return groupRecipient; 517 } 518 519 public long getThreadId() { 520 return threadId; 521 } 522 523 public int getAddedMemberCount() { 524 return addedMemberCount; 525 } 526 527 public @NonNull List<RecipientId> getInvitedMembers() { 528 return invitedMembers; 529 } 530 } 531 532 public enum GroupLinkState { 533 DISABLED, 534 ENABLED, 535 ENABLED_WITH_APPROVAL 536 } 537 538 public enum V2GroupServerStatus { 539 /** The group does not exist. The expected pre-migration state for V1 groups. */ 540 DOES_NOT_EXIST, 541 /** Group exists but self is not in the group. */ 542 NOT_A_MEMBER, 543 /** Self is a full or pending member of the group. */ 544 FULL_OR_PENDING_MEMBER 545 } 546}