That fuck shit the fascists are using
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}