String description,
List<String> members,
List<String> removeMembers,
+ List<String> admins,
+ List<String> removeAdmins,
File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return updateGroup(groupId,
description,
members == null ? null : getSignalServiceAddresses(members),
removeMembers == null ? null : getSignalServiceAddresses(removeMembers),
+ admins == null ? null : getSignalServiceAddresses(admins),
+ removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins),
avatarFile);
}
private Pair<Long, List<SendMessageResult>> updateGroup(
- GroupId groupId,
- String name,
- String description,
- Set<RecipientId> members,
+ final GroupId groupId,
+ final String name,
+ final String description,
+ final Set<RecipientId> members,
final Set<RecipientId> removeMembers,
- File avatarFile
+ final Set<RecipientId> admins,
+ final Set<RecipientId> removeAdmins,
+ final File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
var group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV2) {
- return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile);
+ return updateGroupV2((GroupInfoV2) group,
+ name,
+ description,
+ members,
+ removeMembers,
+ admins,
+ removeAdmins,
+ avatarFile);
}
return updateGroupV1((GroupInfoV1) group, name, members, avatarFile);
final String description,
final Set<RecipientId> members,
final Set<RecipientId> removeMembers,
+ final Set<RecipientId> admins,
+ final Set<RecipientId> removeAdmins,
final File avatarFile
) throws IOException {
Pair<Long, List<SendMessageResult>> result = null;
}
}
+ if (admins != null) {
+ final var newAdmins = new HashSet<>(admins);
+ newAdmins.retainAll(group.getMembers());
+ newAdmins.removeAll(group.getAdminMembers());
+ if (newAdmins.size() > 0) {
+ for (var admin : newAdmins) {
+ var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
+ result = sendUpdateGroupV2Message(group,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+ }
+ }
+
+ if (removeAdmins != null) {
+ final var existingRemoveAdmins = new HashSet<>(removeAdmins);
+ existingRemoveAdmins.retainAll(group.getAdminMembers());
+ if (existingRemoveAdmins.size() > 0) {
+ for (var admin : existingRemoveAdmins) {
+ var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
+ result = sendUpdateGroupV2Message(group,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+ }
+ }
+
if (result == null || name != null || description != null || avatarFile != null) {
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
if (avatarFile != null) {
public Pair<DecryptedGroup, GroupChange> addMembers(
GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
) throws IOException {
- final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
- var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
if (!areMembersValid(newMembers)) {
throw new IOException("Failed to update group");
}
public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
- final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
- final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
return commitChange(groupInfoV2, change);
}
+ public Pair<DecryptedGroup, GroupChange> setMemberAdmin(
+ GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
+ ) throws IOException {
+ final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
+ final var address = addressResolver.resolveSignalServiceAddress(recipientId);
+ final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
+ final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole);
+ return commitChange(groupInfoV2, change);
+ }
+
+ private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
+ final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ return groupsV2Operations.forGroup(groupSecretParams);
+ }
+
private Pair<DecryptedGroup, GroupChange> revokeInvites(
GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
) throws IOException {
- final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
- final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
final var uuidCipherTexts = pendingMembers.stream().map(member -> {
try {
return new UuidCiphertext(member.getUuidCipherText().toByteArray());
private Pair<DecryptedGroup, GroupChange> ejectMembers(
GroupInfoV2 groupInfoV2, Set<UUID> uuids
) throws IOException {
- final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
- final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
}
return Set.of();
}
+ public Set<RecipientId> getAdminMembers() {
+ return Set.of();
+ }
+
public abstract boolean isBlocked();
public abstract void setBlocked(boolean blocked);
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.storageservice.protos.groups.AccessControl;
+import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
.collect(Collectors.toSet());
}
+ @Override
+ public Set<RecipientId> getAdminMembers() {
+ if (this.group == null) {
+ return Set.of();
+ }
+ return group.getMembersList()
+ .stream()
+ .filter(m -> m.getRole() == Member.Role.ADMINISTRATOR)
+ .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .map(recipientResolver::resolveRecipient)
+ .collect(Collectors.toSet());
+ }
+
@Override
public boolean isBlocked() {
return blocked;
final var groupInviteLink = group.getGroupInviteLink();
writer.println(
- "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}",
+ "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Link: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.getDescription(),
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
resolveMembers(m, group.getRequestingMembers()),
+ resolveMembers(m, group.getAdminMembers()),
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else {
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
resolveMembers(m, group.getRequestingMembers()),
+ resolveMembers(m, group.getAdminMembers()),
groupInviteLink == null ? null : groupInviteLink.getUrl()));
}
public Set<String> members;
public Set<String> pendingMembers;
public Set<String> requestingMembers;
+ public Set<String> admins;
public String groupInviteLink;
public JsonGroup(
Set<String> members,
Set<String> pendingMembers,
Set<String> requestingMembers,
+ Set<String> admins,
String groupInviteLink
) {
this.id = id;
this.members = members;
this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers;
+ this.admins = admins;
this.groupInviteLink = groupInviteLink;
}
}
subparser.addArgument("-r", "--remove-member")
.nargs("*")
.help("Specify one or more members to remove from the group");
+ subparser.addArgument("--admin").nargs("*").help("Specify one or more members to make a group admin");
+ subparser.addArgument("--remove-admin")
+ .nargs("*")
+ .help("Specify one or more members to remove group admin privileges");
+
}
@Override
List<String> groupRemoveMembers = ns.getList("remove-member");
+ List<String> groupAdmins = ns.getList("admin");
+
+ List<String> groupRemoveAdmins = ns.getList("remove-admin");
+
var groupAvatar = ns.getString("avatar");
try {
groupDescription,
groupMembers,
groupRemoveMembers,
+ groupAdmins,
+ groupRemoveAdmins,
groupAvatar == null ? null : new File(groupAvatar));
ErrorUtils.handleTimestampAndSendMessageResults(writer, results.first(), results.second());
}
null,
members,
null,
+ null,
+ null,
avatar == null ? null : new File(avatar));
checkSendMessageResults(results.first(), results.second());
return groupId;