diff --git a/CHANGELOG.md b/CHANGELOG.md index 31550332..7567a29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +## [0.14.1] - 2026-03-08 + +### Added + +- Added isArchived to contact json output (Thanks @moppman) +- Added support for group member labels + +### Fixed + +- Adapt registration to signal server changes + ## [0.14.0] - 2026-03-01 **Attention**: Now requires Java 25 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c18cd8a8..7162e881 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ If you have a question you can ask it in the [GitHub discussions page](https://g - Be sure to include a **title and clear description**, as much relevant information as possible. - Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using - Specify if it's the normal java or the graalvm native version. - - Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report + - Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report # Pull request diff --git a/build.gradle.kts b/build.gradle.kts index 916ee4f1..286b5b94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,12 +3,12 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "0.11.4" + id("org.graalvm.buildtools.native") version "0.11.5" } allprojects { group = "org.asamk" - version = "0.14.1-SNAPSHOT" + version = "0.14.2-SNAPSHOT" } java { diff --git a/client/Cargo.lock b/client/Cargo.lock index 045b10c1..bd3dfd02 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -897,9 +897,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", diff --git a/client/src/cli.rs b/client/src/cli.rs index fdf31e6f..d7598b9a 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -598,6 +598,12 @@ pub enum CliCommands { #[arg(short = 'e', long)] expiration: Option, + + #[arg(long = "member-label-emoji")] + member_label_emoji: Option, + + #[arg(long = "member-label")] + member_label: Option, }, UpdateProfile { #[arg(long = "given-name")] diff --git a/client/src/jsonrpc.rs b/client/src/jsonrpc.rs index ad072878..3ebe710b 100644 --- a/client/src/jsonrpc.rs +++ b/client/src/jsonrpc.rs @@ -460,6 +460,8 @@ pub trait Rpc { #[allow(non_snake_case)] setPermissionEditDetails: Option, #[allow(non_snake_case)] setPermissionSendMessages: Option, expiration: Option, + #[allow(non_snake_case)] memberLabelEmoji: Option, + #[allow(non_snake_case)] memberLabel: Option, ) -> Result; #[method(name = "updateProfile", param_kind = map)] diff --git a/client/src/main.rs b/client/src/main.rs index f389630e..e025c870 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -535,6 +535,8 @@ async fn handle_command( set_permission_edit_details, set_permission_send_messages, expiration, + member_label_emoji, + member_label, } => { client .update_group( @@ -568,6 +570,8 @@ async fn handle_command( GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(), }), expiration, + member_label_emoji, + member_label, ) .await } diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index f189a424..15df456a 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,9 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.1 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3df9835e..dbf9b4dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_140" +signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_141" sqlite = "org.xerial:sqlite-jdbc:3.51.2.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a65..d997cfc6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6a..dbc3ce4a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java index 4d1fdaa1..fc668684 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Group.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -12,10 +12,9 @@ public record Group( String title, String description, GroupInviteLinkUrl groupInviteLinkUrl, - Set members, + Set members, Set pendingMembers, Set requestingMembers, - Set adminMembers, Set bannedMembers, boolean isBlocked, int messageExpirationTimer, @@ -37,8 +36,7 @@ public record Group( groupInfo.getGroupInviteLink(), groupInfo.getMembers() .stream() - .map(recipientStore::resolveRecipientAddress) - .map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress) + .map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore)) .collect(Collectors.toSet()), groupInfo.getPendingMembers() .stream() @@ -50,11 +48,6 @@ public record Group( .map(recipientStore::resolveRecipientAddress) .map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress) .collect(Collectors.toSet()), - groupInfo.getAdminMembers() - .stream() - .map(recipientStore::resolveRecipientAddress) - .map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress) - .collect(Collectors.toSet()), groupInfo.getBannedMembers() .stream() .map(recipientStore::resolveRecipientAddress) diff --git a/lib/src/main/java/org/asamk/signal/manager/api/GroupMember.java b/lib/src/main/java/org/asamk/signal/manager/api/GroupMember.java new file mode 100644 index 00000000..239a597c --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/GroupMember.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.groups.GroupMemberInfo; + +public record GroupMember( + RecipientAddress recipientAddress, boolean isAdmin, String labelEmoji, String label +) { + + public static GroupMember from(final GroupMemberInfo memberInfo, final RecipientAddressResolver recipientStore) { + return new GroupMember(recipientStore.resolveRecipientAddress(memberInfo.getRecipientId()) + .toApiRecipientAddress(), memberInfo.isAdmin(), memberInfo.labelEmoji(), memberInfo.labelString()); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java index 3080b8fb..b9d60b00 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Message.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java @@ -7,6 +7,7 @@ public record Message( String messageText, List attachments, boolean viewOnce, + boolean voiceNote, List mentions, Optional quote, Optional sticker, diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java index c1ba4270..9a79e397 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java @@ -19,6 +19,8 @@ public class UpdateGroup { private final String avatarFile; private final Integer expirationTimer; private final Boolean isAnnouncementGroup; + private final String labelEmoji; + private final String labelString; private UpdateGroup(final Builder builder) { name = builder.name; @@ -36,6 +38,8 @@ public class UpdateGroup { avatarFile = builder.avatarFile; expirationTimer = builder.expirationTimer; isAnnouncementGroup = builder.isAnnouncementGroup; + labelEmoji = builder.labelEmoji; + labelString = builder.labelString; } public static Builder newBuilder() { @@ -57,7 +61,9 @@ public class UpdateGroup { copy.editDetailsPermission, copy.avatarFile, copy.expirationTimer, - copy.isAnnouncementGroup); + copy.isAnnouncementGroup, + copy.labelEmoji, + copy.labelString); } public static Builder newBuilder( @@ -75,7 +81,9 @@ public class UpdateGroup { final GroupPermission editDetailsPermission, final String avatarFile, final Integer expirationTimer, - final Boolean isAnnouncementGroup + final Boolean isAnnouncementGroup, + final String labelEmoji, + final String labelString ) { return new Builder(name, description, @@ -91,7 +99,9 @@ public class UpdateGroup { editDetailsPermission, avatarFile, expirationTimer, - isAnnouncementGroup); + isAnnouncementGroup, + labelEmoji, + labelString); } public String getName() { @@ -154,6 +164,14 @@ public class UpdateGroup { return isAnnouncementGroup; } + public String getLabelEmoji() { + return labelEmoji; + } + + public String getLabelString() { + return labelString; + } + public static final class Builder { private String name; @@ -171,6 +189,8 @@ public class UpdateGroup { private String avatarFile; private Integer expirationTimer; private Boolean isAnnouncementGroup; + private String labelEmoji; + private String labelString; private Builder() { } @@ -190,7 +210,9 @@ public class UpdateGroup { final GroupPermission editDetailsPermission, final String avatarFile, final Integer expirationTimer, - final Boolean isAnnouncementGroup + final Boolean isAnnouncementGroup, + final String labelEmoji, + final String labelString ) { this.name = name; this.description = description; @@ -207,6 +229,8 @@ public class UpdateGroup { this.avatarFile = avatarFile; this.expirationTimer = expirationTimer; this.isAnnouncementGroup = isAnnouncementGroup; + this.labelEmoji = labelEmoji; + this.labelString = labelString; } public Builder withName(final String val) { @@ -284,6 +308,16 @@ public class UpdateGroup { return this; } + public Builder withLabelEmoji(final String val) { + labelEmoji = val; + return this; + } + + public Builder withLabelString(final String val) { + labelString = val; + return this; + } + public UpdateGroup build() { return new UpdateGroup(this); } diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 90cea121..c2109f86 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -20,6 +20,7 @@ public class ServiceConfig { public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; public static final long MAX_ENVELOPE_SIZE = 0; + public static final int MAX_INCREMENTAL_MACS_PER_ENVELOPE = 10; public static final int MAX_MESSAGE_SIZE_BYTES = 2000; public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; public static final boolean AUTOMATIC_NETWORK_RETRY = true; @@ -29,7 +30,7 @@ public class ServiceConfig { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var attachmentBackfill = !isPrimaryDevice; - final var spqr = !isPrimaryDevice; + final var spqr = true; return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 08526c72..cc2ba7d0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -44,8 +44,8 @@ public class AttachmentHelper { return attachmentStore.retrieveAttachment(id); } - public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { - final var attachmentStreams = createAttachmentStreams(attachments); + public List uploadAttachments(final List attachments, boolean voiceNote) throws AttachmentInvalidException, IOException { + final var attachmentStreams = createAttachmentStreams(attachments, voiceNote); try { // Upload attachments here, so we only upload once even for multiple recipients @@ -61,14 +61,18 @@ public class AttachmentHelper { } } - private List createAttachmentStreams(List attachments) throws AttachmentInvalidException, IOException { + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { + return uploadAttachments(attachments, false); + } + + private List createAttachmentStreams(List attachments, boolean voiceNote) throws AttachmentInvalidException, IOException { if (attachments == null) { return null; } final var signalServiceAttachments = new ArrayList(attachments.size()); for (var attachment : attachments) { final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec(); - signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, uploadSpec)); + signalServiceAttachments.add(AttachmentUtils.createAttachmentStream(attachment, voiceNote, uploadSpec)); } return signalServiceAttachments; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 516ff1e1..c0fcca45 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -299,7 +299,9 @@ public class GroupHelper { final GroupPermission editDetailsPermission, final String avatarFile, final Integer expirationTimer, - final Boolean isAnnouncementGroup + final Boolean isAnnouncementGroup, + final String labelEmoji, + final String labelString ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { var group = getGroupForUpdating(groupId); final var avatarBytes = readAvatarBytes(avatarFile); @@ -323,7 +325,9 @@ public class GroupHelper { editDetailsPermission, avatarBytes, expirationTimer, - isAnnouncementGroup); + isAnnouncementGroup, + labelEmoji, + labelString); } catch (ConflictException e) { // Detected conflicting update, refreshing group and trying again group = getGroup(groupId, true); @@ -342,7 +346,9 @@ public class GroupHelper { editDetailsPermission, avatarBytes, expirationTimer, - isAnnouncementGroup); + isAnnouncementGroup, + labelEmoji, + labelString); } } @@ -701,7 +707,9 @@ public class GroupHelper { final GroupPermission editDetailsPermission, final byte[] avatarFile, final Integer expirationTimer, - final Boolean isAnnouncementGroup + final Boolean isAnnouncementGroup, + final String labelEmoji, + final String labelString ) throws IOException { SendGroupMessageResults result = null; final var groupV2Helper = context.getGroupV2Helper(); @@ -731,7 +739,7 @@ public class GroupHelper { if (banMembers != null) { existingRemoveMembers.addAll(banMembers); } - existingRemoveMembers.retainAll(group.getMembers()); + existingRemoveMembers.retainAll(group.getMemberRecipientIds()); if (members != null) { existingRemoveMembers.removeAll(members); } @@ -757,8 +765,8 @@ public class GroupHelper { if (admins != null) { final var newAdmins = new HashSet<>(admins); - newAdmins.retainAll(group.getMembers()); - newAdmins.removeAll(group.getAdminMembers()); + newAdmins.retainAll(group.getMemberRecipientIds()); + newAdmins.removeAll(group.getAdminMemberRecipientIds()); if (!newAdmins.isEmpty()) { for (var admin : newAdmins) { var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); @@ -771,7 +779,7 @@ public class GroupHelper { if (removeAdmins != null) { final var existingRemoveAdmins = new HashSet<>(removeAdmins); - existingRemoveAdmins.retainAll(group.getAdminMembers()); + existingRemoveAdmins.retainAll(group.getAdminMemberRecipientIds()); if (!existingRemoveAdmins.isEmpty()) { for (var admin : existingRemoveAdmins) { var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); @@ -830,6 +838,15 @@ public class GroupHelper { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } + if (labelString != null || labelEmoji != null) { + final var selfRecipientId = account.getSelfRecipientId(); + final var selfMember = group.getMember(selfRecipientId); + var groupGroupChangePair = groupV2Helper.setMemberLabels(group, + labelEmoji != null ? labelEmoji : selfMember.labelEmoji(), + labelString != null ? labelString : selfMember.labelString()); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + if (name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { @@ -859,9 +876,9 @@ public class GroupHelper { final GroupInfoV2 groupInfoV2, final Set newAdmins ) throws LastGroupAdminException, IOException { - final var currentAdmins = groupInfoV2.getAdminMembers(); + final var currentAdmins = groupInfoV2.getAdminMemberRecipientIds(); newAdmins.removeAll(currentAdmins); - newAdmins.retainAll(groupInfoV2.getMembers()); + newAdmins.retainAll(groupInfoV2.getMemberRecipientIds()); if (currentAdmins.contains(account.getSelfRecipientId()) && currentAdmins.size() == 1 && groupInfoV2.getMembers().size() > 1 @@ -888,7 +905,7 @@ public class GroupHelper { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.getGroupId().serialize()) .withName(g.name) - .withMembers(g.getMembers() + .withMembers(g.getMemberRecipientIds() .stream() .map(context.getRecipientHelper()::resolveSignalServiceAddress) .toList()); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index b5b76c16..437ddb67 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -533,6 +533,18 @@ class GroupV2Helper { return commitChange(groupInfoV2, change); } + Pair setMemberLabels( + GroupInfoV2 groupInfoV2, + String labelEmoji, + String labelString + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createChangeMemberLabel(getSelfAci(), + labelString == null ? "" : labelString, + labelEmoji); + return commitChange(groupInfoV2, change); + } + private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) { return switch (state) { case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 10259350..20d74da9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -320,7 +320,9 @@ public class SendHelper { messageBuilder.withExpiration(g.getMessageExpirationTimer()); final var message = messageBuilder.build(); - final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId()); + final var recipients = includeSelf + ? g.getMemberRecipientIds() + : g.getMembersWithout(account.getSelfRecipientId()); if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { if (message.getBody().isPresent() diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index fe369645..a13fc38f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -114,7 +114,7 @@ public class SyncHelper { if (record instanceof GroupInfoV1 groupInfo) { out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.ofNullable(groupInfo.name), - groupInfo.getMembers() + groupInfo.getMemberRecipientIds() .stream() .map(context.getRecipientHelper()::resolveSignalServiceAddress) .toList(), diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 3e614428..bd920635 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -643,7 +643,9 @@ public class ManagerImpl implements Manager { updateGroup.getEditDetailsPermission(), updateGroup.getAvatarFile(), updateGroup.getExpirationTimer(), - updateGroup.getIsAnnouncementGroup()); + updateGroup.getIsAnnouncementGroup(), + updateGroup.getLabelEmoji(), + updateGroup.getLabelString()); } @Override @@ -851,7 +853,7 @@ public class ManagerImpl implements Manager { messageBuilder.withBody(message.messageText()); } if (!message.attachments().isEmpty()) { - final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments()); + final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments(), message.voiceNote()); if (!additionalAttachments.isEmpty()) { additionalAttachments.addAll(uploadedAttachments); messageBuilder.withAttachments(additionalAttachments); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index b94bf8ba..9c6c991d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -337,6 +337,7 @@ public class SignalDependencies { Optional.empty(), executor, ServiceConfig.MAX_ENVELOPE_SIZE, + ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE, () -> true, true, true)); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 7de3a8f9..25142c26 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.api.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.whispersystems.signalservice.api.push.DistributionId; +import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,7 +25,18 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 { public abstract GroupInviteLinkUrl getGroupInviteLink(); - public abstract Set getMembers(); + public abstract Collection getMembers(); + + public Set getMemberRecipientIds() { + return getMembers().stream().map(GroupMemberInfo::getRecipientId).collect(Collectors.toSet()); + } + + public GroupMemberInfo getMember(RecipientId recipientId) { + return getMembers().stream() + .filter(member -> member.getRecipientId().equals(recipientId)) + .findFirst() + .orElseThrow(); + } public Set getBannedMembers() { return Set.of(); @@ -38,7 +50,7 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 { return Set.of(); } - public Set getAdminMembers() { + public Set getAdminMemberRecipientIds() { return Set.of(); } @@ -61,21 +73,23 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 { public abstract GroupPermission getPermissionSendMessage(); public Set getMembersWithout(RecipientId recipientId) { - return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); + return getMemberRecipientIds().stream() + .filter(member -> !member.equals(recipientId)) + .collect(Collectors.toSet()); } public Set getMembersIncludingPendingWithout(RecipientId recipientId) { - return Stream.concat(getMembers().stream(), getPendingMembers().stream()) + return Stream.concat(getMemberRecipientIds().stream(), getPendingMembers().stream()) .filter(member -> !member.equals(recipientId)) .collect(Collectors.toSet()); } public boolean isMember(RecipientId recipientId) { - return getMembers().contains(recipientId); + return getMembers().stream().anyMatch(m -> m.getRecipientId().equals(recipientId)); } public boolean isAdmin(RecipientId recipientId) { - return getAdminMembers().contains(recipientId); + return getMembers().stream().anyMatch(m -> m.isAdmin() && m.getRecipientId().equals(recipientId)); } public boolean isPendingMember(RecipientId recipientId) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index f377ff2d..2b601ed8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -80,8 +80,8 @@ public final class GroupInfoV1 extends GroupInfo { return null; } - public Set getMembers() { - return new HashSet<>(members); + public Collection getMembers() { + return members.stream().map(m -> (GroupMemberInfo) new GroupMemberInfoV1(m)).toList(); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index dbb123c5..3616dceb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -3,18 +3,17 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.api.GroupIdV2; import org.asamk.signal.manager.api.GroupInviteLinkUrl; import org.asamk.signal.manager.api.GroupPermission; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.signal.core.models.ServiceId; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.storageservice.storage.protos.groups.AccessControl; -import org.signal.storageservice.storage.protos.groups.Member; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup; import org.signal.storageservice.storage.protos.groups.local.EnabledState; import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; @@ -122,14 +121,11 @@ public final class GroupInfoV2 extends GroupInfo { } @Override - public Set getMembers() { + public Collection getMembers() { if (this.group == null) { return Set.of(); } - return group.members.stream() - .map(m -> ServiceId.parseOrThrow(m.aciBytes)) - .map(recipientResolver::resolveRecipient) - .collect(Collectors.toSet()); + return group.members.stream().map(m -> (GroupMemberInfo) new GroupMemberInfoV2(m, recipientResolver)).toList(); } @Override @@ -175,16 +171,11 @@ public final class GroupInfoV2 extends GroupInfo { } @Override - public Set getAdminMembers() { - if (this.group == null) { - return Set.of(); - } - return group.members.stream() - .filter(m -> m.role == Member.Role.ADMINISTRATOR) - .map(m -> new RecipientAddress(ServiceId.ACI.parseOrNull(m.aciBytes), - ServiceId.PNI.parseOrNull(m.pniBytes), - null)) - .map(recipientResolver::resolveRecipient) + public Set getAdminMemberRecipientIds() { + return this.getMembers() + .stream() + .filter(GroupMemberInfo::isAdmin) + .map(GroupMemberInfo::getRecipientId) .collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfo.java new file mode 100644 index 00000000..d0d82c87 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfo.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.storage.groups; + +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public interface GroupMemberInfo { + + RecipientId getRecipientId(); + + default boolean isAdmin() { + return false; + } + + default String labelEmoji() { + return null; + } + + default String labelString() { + return null; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV1.java new file mode 100644 index 00000000..c24661b4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV1.java @@ -0,0 +1,17 @@ +package org.asamk.signal.manager.storage.groups; + +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class GroupMemberInfoV1 implements GroupMemberInfo { + + private final RecipientId recipientId; + + public GroupMemberInfoV1(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public RecipientId getRecipientId() { + return this.recipientId; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV2.java new file mode 100644 index 00000000..88557ff1 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupMemberInfoV2.java @@ -0,0 +1,38 @@ +package org.asamk.signal.manager.storage.groups; + +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.signal.core.models.ServiceId; +import org.signal.storageservice.storage.protos.groups.Member; +import org.signal.storageservice.storage.protos.groups.local.DecryptedMember; + +public class GroupMemberInfoV2 implements GroupMemberInfo { + + private final RecipientResolver recipientResolver; + private final DecryptedMember member; + + public GroupMemberInfoV2(final DecryptedMember member, final RecipientResolver recipientResolver) { + this.recipientResolver = recipientResolver; + this.member = member; + } + + @Override + public RecipientId getRecipientId() { + return recipientResolver.resolveRecipient(ServiceId.ACI.parseOrThrow(member.aciBytes)); + } + + @Override + public boolean isAdmin() { + return member.role == Member.Role.ADMINISTRATOR; + } + + @Override + public String labelEmoji() { + return member.labelEmoji.isEmpty() ? null : member.labelEmoji; + } + + @Override + public String labelString() { + return member.labelString.isEmpty() ? null : member.labelString; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index f726b7f0..2c4bad99 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -648,7 +648,7 @@ public class GroupStore { ON CONFLICT (group_id, recipient_id) DO NOTHING """.formatted(TABLE_GROUP_V1_MEMBER); try (final var statement = connection.prepareStatement(sqlInsertMember)) { - for (final var recipient : groupV1.getMembers()) { + for (final var recipient : groupV1.getMemberRecipientIds()) { statement.setLong(1, internalId); statement.setLong(2, recipient.id()); statement.executeUpdate(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java index b6d79c2b..71892048 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java @@ -133,6 +133,11 @@ public class KyberPreKeyStore implements SignalServiceKyberPreKeyStore { return getPreKey(keyId) != null; } + /** + * When we mark Kyber pre-keys used, we want to keep a record of last resort tuples, which are deleted when the key + * itself is deleted from this table via a cascading delete. + * For non-last-resort keys, this method just deletes them like normal. + */ @Override public void markKyberPreKeyUsed( final int kyberPreKeyId, diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java index bab4c433..5e2a3c6b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -14,32 +14,49 @@ public class AttachmentUtils { public static SignalServiceAttachmentStream createAttachmentStream( String attachment, + boolean voiceNote, ResumableUploadSpec resumableUploadSpec ) throws AttachmentInvalidException { try { final var streamDetails = Utils.createStreamDetails(attachment); - return createAttachmentStream(streamDetails.first(), streamDetails.second(), resumableUploadSpec); + return createAttachmentStream(streamDetails.first(), streamDetails.second(), voiceNote, resumableUploadSpec); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } } + public static SignalServiceAttachmentStream createAttachmentStream( + String attachment, + ResumableUploadSpec resumableUploadSpec + ) throws AttachmentInvalidException { + return createAttachmentStream(attachment, false, resumableUploadSpec); + } + + public static SignalServiceAttachmentStream createAttachmentStream( + StreamDetails streamDetails, + Optional name, + boolean voiceNote, + ResumableUploadSpec resumableUploadSpec + ) throws ResumeLocationInvalidException { + final var uploadTimestamp = System.currentTimeMillis(); + return SignalServiceAttachmentStream.newStreamBuilder() + .withStream(streamDetails.getStream()) + .withContentType(streamDetails.getContentType()) + .withLength(streamDetails.getLength()) + .withFileName(name.orElse(null)) + .withVoiceNote(voiceNote) + .withUploadTimestamp(uploadTimestamp) + .withResumableUploadSpec(resumableUploadSpec) + .withUuid(UUID.randomUUID()) + .build(); + } + public static SignalServiceAttachmentStream createAttachmentStream( StreamDetails streamDetails, Optional name, ResumableUploadSpec resumableUploadSpec ) throws ResumeLocationInvalidException { - // TODO maybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option - final var uploadTimestamp = System.currentTimeMillis(); - return SignalServiceAttachmentStream.newStreamBuilder() - .withStream(streamDetails.getStream()) - .withContentType(streamDetails.getContentType()) - .withLength(streamDetails.getLength()) - .withFileName(name.orElse(null)) - .withUploadTimestamp(uploadTimestamp) - .withResumableUploadSpec(resumableUploadSpec) - .withUuid(UUID.randomUUID()) - .build(); + return createAttachmentStream(streamDetails, name, false, resumableUploadSpec); } } diff --git a/man/signal-cli-jsonrpc.5.adoc b/man/signal-cli-jsonrpc.5.adoc index 8273a442..1c035af8 100644 --- a/man/signal-cli-jsonrpc.5.adoc +++ b/man/signal-cli-jsonrpc.5.adoc @@ -154,7 +154,7 @@ RESPONSE: `{"jsonrpc":"2.0","result":{"timestamp":999},"id":4}` --- -REQUEST: `{"jsonrpc":"2.0","method":"updateGroup","params":{"groupId":"GROUP_ID=","name":"new group name","members":["+ZZZ"],"link":"enabledWithApproval","setPermissionEditDetails":"only-admins"},"id":"someId"}` +REQUEST: `{"jsonrpc":"2.0","method":"updateGroup","params":{"groupId":"GROUP_ID=","name":"new group name","members":["+ZZZ"],"link":"enabledWithApproval","setPermissionEditDetails":"only-admins","memberLabelEmoji":"😀","memberLabel":"My Label"},"id":"someId"}` RESPONSE: `{"jsonrpc":"2.0","result":{"timestamp":9999},"id":"someId"}` diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 969171c3..1c7d1b66 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -443,6 +443,7 @@ By default, recipients can select multiple options. *-o* OPTION [OPTION ...], *--option* OPTION [OPTION ...]*:: The options for the poll. Between 2 and 10 options must be specified. +Each option must be between 1 and 100 characters. === sendPollVote @@ -754,6 +755,12 @@ Groups where only admins can send messages are also called announcement groups Set expiration time of messages (seconds). To disable expiration set expiration time to 0. +*--member-label-emoji* EMOJI:: +Specify the emoji for the member label. + +*--member-label* STRING:: +Specify the string for the member label. + === quitGroup Send a quit group message to all group members and remove self from member list. diff --git a/run_tests.sh b/run_tests.sh index 4ca0ad21..81d2c007 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -50,9 +50,9 @@ run() { if [ "$JSON_RPC" -eq 1 ]; then "$SIGNAL_CLI" $@ elif [ "$DBUS" -eq 1 ]; then - "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v '^Warning:' | grep -v 'at org' + "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' else - "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v '^Warning:' | grep -v 'at org' + "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' fi set +x } diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 77fe22eb..9084e907 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -1,5 +1,7 @@ package org.asamk.signal.commands; +import com.fasterxml.jackson.annotation.JsonInclude; + import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -7,6 +9,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.GroupMember; import org.asamk.signal.manager.api.RecipientAddress; import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.OutputWriter; @@ -37,33 +40,55 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { subparser.addArgument("-g", "--group-id").help("Specify one or more group IDs to show.").nargs("*"); } - private static Set resolveMembers(Set addresses) { + private static Set resolveMembers(Set addresses) { + return addresses.stream() + .map(m -> m.recipientAddress().getLegacyIdentifier() + (m.isAdmin() ? "{ADMIN}" : "") + ( + m.labelEmoji() != null || m.label() != null ? "(" + ( + m.labelEmoji() != null ? m.labelEmoji() : "" + ) + ( + m.label() != null ? m.label() : "" + ) + ")" : "" + )) + .collect(Collectors.toSet()); + } + + private static Set resolveMemberAddress(Set addresses) { return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet()); } - private static Set resolveJsonMembers(Set addresses) { + private static Set resolveJsonMembers(Set addresses) { return addresses.stream() - .map(address -> new JsonGroupMember(address.number().orElse(null), + .map(address -> new JsonGroupMemberAddress(address.number().orElse(null), address.uuid().map(UUID::toString).orElse(null))) .collect(Collectors.toSet()); } + private static Set resolveFullJsonMembers(Set addresses) { + return addresses.stream().map(member -> { + final var address = member.recipientAddress(); + return new JsonGroupMember(address.number().orElse(null), + address.uuid().map(UUID::toString).orElse(null), + member.isAdmin(), + member.labelEmoji(), + member.label()); + }).collect(Collectors.toSet()); + } + private static void printGroupPlainText(PlainTextWriter writer, Group group, boolean detailed) { if (detailed) { final var groupInviteLink = group.groupInviteLinkUrl(); writer.println( - "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Banned: {} Message expiration: {} Link: {}", + "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}", group.groupId().toBase64(), group.title(), group.description(), group.isMember(), group.isBlocked(), resolveMembers(group.members()), - resolveMembers(group.pendingMembers()), - resolveMembers(group.requestingMembers()), - resolveMembers(group.adminMembers()), - resolveMembers(group.bannedMembers()), + resolveMemberAddress(group.pendingMembers()), + resolveMemberAddress(group.requestingMembers()), + resolveMemberAddress(group.bannedMembers()), group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { @@ -96,10 +121,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { group.isMember(), group.isBlocked(), group.messageExpirationTimer(), - resolveJsonMembers(group.members()), + resolveFullJsonMembers(group.members()), resolveJsonMembers(group.pendingMembers()), resolveJsonMembers(group.requestingMembers()), - resolveJsonMembers(group.adminMembers()), + resolveJsonMembers(group.members() + .stream() + .filter(GroupMember::isAdmin) + .map(GroupMember::recipientAddress) + .collect(Collectors.toSet())), resolveJsonMembers(group.bannedMembers()), group.permissionAddMember().name(), group.permissionEditDetails().name(), @@ -125,15 +154,23 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { boolean isBlocked, int messageExpirationTime, Set members, - Set pendingMembers, - Set requestingMembers, - Set admins, - Set banned, + Set pendingMembers, + Set requestingMembers, + @Deprecated Set admins, + Set banned, String permissionAddMember, String permissionEditDetails, String permissionSendMessage, String groupInviteLink ) {} - private record JsonGroupMember(String number, String uuid) {} + private record JsonGroupMemberAddress(String number, String uuid) {} + + private record JsonGroupMember( + String number, + String uuid, + boolean isAdmin, + @JsonInclude(JsonInclude.Include.NON_NULL) String labelEmoji, + @JsonInclude(JsonInclude.Include.NON_NULL) String label + ) {} } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 21e39d3f..aa4bb957 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -109,6 +109,9 @@ public class SendCommand implements JsonRpcLocalCommand { .action(Arguments.storeTrue()) .help("Send the message without the urgent flag, so no push notification is triggered for the recipient. " + "The message will still be delivered in real-time if the recipient's app is active."); + subparser.addArgument("--voice-note") + .action(Arguments.storeTrue()) + .help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients."); } @Override @@ -171,6 +174,7 @@ public class SendCommand implements JsonRpcLocalCommand { attachments = List.of(); } final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once")); + final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note")); final var selfNumber = m.getSelfNumber(); @@ -247,6 +251,7 @@ public class SendCommand implements JsonRpcLocalCommand { final var message = new Message(messageText, attachments, viewOnce, + voiceNote, mentions, Optional.ofNullable(quote), Optional.ofNullable(sticker), diff --git a/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java b/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java index 02b89e5e..08b24560 100644 --- a/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendPollCreateCommand.java @@ -25,6 +25,7 @@ public class SendPollCreateCommand implements JsonRpcLocalCommand { private static final Logger logger = LoggerFactory.getLogger(SendPollCreateCommand.class); private static final int MAX_POLL_OPTIONS = 10; + private static final int MAX_POLL_OPTION_LENGTH = 100; @Override public String getName() { @@ -76,6 +77,14 @@ public class SendPollCreateCommand implements JsonRpcLocalCommand { if (options.size() > MAX_POLL_OPTIONS) { throw new UserErrorException("Poll cannot have more than " + MAX_POLL_OPTIONS + " options"); } + for (final var option : options) { + if (option.isEmpty()) { + throw new UserErrorException("Poll options must not be empty"); + } + if (option.length() > MAX_POLL_OPTION_LENGTH) { + throw new UserErrorException("Poll option \"" + option + "\" exceeds the maximum length of " + MAX_POLL_OPTION_LENGTH + " characters"); + } + } try { var results = m.sendPollCreateMessage(question, !noMulti, options, recipientIdentifiers, notifySelf); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 1231d3d4..7f93cf04 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -75,6 +75,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { .choices("every-member", "only-admins"); subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); + subparser.addArgument("--member-label-emoji").help("Specify the emoji for the member label."); + subparser.addArgument("--member-label").help("Specify the string for the member label."); } GroupLinkState getGroupLinkState(String value) throws UserErrorException { @@ -126,6 +128,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member")); var groupEditDetailsPermission = getGroupPermission(ns.getString("set-permission-edit-details")); var groupSendMessagesPermission = getGroupPermission(ns.getString("set-permission-send-messages")); + var memberLabelEmoji = ns.getString("member-label-emoji"); + var memberLabelString = ns.getString("member-label"); try { boolean isNewGroup = false; @@ -159,6 +163,8 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { .withIsAnnouncementGroup(groupSendMessagesPermission == null ? null : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS) + .withLabelEmoji(memberLabelEmoji) + .withLabelString(memberLabelString) .build()); if (results != null) { if (groupMessageResults == null) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index aeebc783..70a6f146 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -12,6 +12,7 @@ import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupInviteLinkUrl; +import org.asamk.signal.manager.api.GroupMember; import org.asamk.signal.manager.api.GroupNotFoundException; import org.asamk.signal.manager.api.GroupPermission; import org.asamk.signal.manager.api.GroupSendingNotAllowedException; @@ -834,24 +835,22 @@ public class DbusManagerImpl implements Manager { final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group"); final var id = (byte[]) group.get("Id").getValue(); try { + final var admins = new HashSet<>(((List) group.get("Admins").getValue())); return new Group(GroupId.unknownVersion(id), (String) group.get("Name").getValue(), (String) group.get("Description").getValue(), GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()), ((List) group.get("Members").getValue()).stream() - .map(m -> new RecipientAddress(m)) + .map(m -> new GroupMember(new RecipientAddress(m), admins.contains(m), null, null)) .collect(Collectors.toSet()), ((List) group.get("PendingMembers").getValue()).stream() - .map(m -> new RecipientAddress(m)) + .map(RecipientAddress::new) .collect(Collectors.toSet()), ((List) group.get("RequestingMembers").getValue()).stream() - .map(m -> new RecipientAddress(m)) - .collect(Collectors.toSet()), - ((List) group.get("Admins").getValue()).stream() - .map(m -> new RecipientAddress(m)) + .map(RecipientAddress::new) .collect(Collectors.toSet()), ((List) group.get("Banned").getValue()).stream() - .map(m -> new RecipientAddress(m)) + .map(RecipientAddress::new) .collect(Collectors.toSet()), (boolean) group.get("IsBlocked").getValue(), (int) group.get("MessageExpirationTimer").getValue(), diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 7a8b034a..2f4f881a 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupInviteLinkUrl; import org.asamk.signal.manager.api.GroupLinkState; +import org.asamk.signal.manager.api.GroupMember; import org.asamk.signal.manager.api.GroupNotFoundException; import org.asamk.signal.manager.api.GroupPermission; import org.asamk.signal.manager.api.GroupSendingNotAllowedException; @@ -237,6 +238,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(), @@ -403,6 +405,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(), @@ -450,6 +453,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { final var message = new Message(messageText, attachments, false, + false, List.of(), Optional.empty(), Optional.empty(), @@ -624,7 +628,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { if (group == null) { return List.of(); } else { - final var members = group.members(); + final var members = group.members().stream().map(GroupMember::recipientAddress).collect(Collectors.toSet()); return getRecipientStrings(members); } } @@ -1300,13 +1304,20 @@ public class DbusSignalImpl implements Signal, AutoCloseable { () -> getGroup().messageExpirationTimer(), this::setMessageExpirationTime), new DbusProperty<>("Members", - () -> new Variant<>(getRecipientStrings(getGroup().members()), "as")), + () -> new Variant<>(getRecipientStrings(getGroup().members() + .stream() + .map(GroupMember::recipientAddress) + .collect(Collectors.toSet())), "as")), new DbusProperty<>("PendingMembers", () -> new Variant<>(getRecipientStrings(getGroup().pendingMembers()), "as")), new DbusProperty<>("RequestingMembers", () -> new Variant<>(getRecipientStrings(getGroup().requestingMembers()), "as")), new DbusProperty<>("Admins", - () -> new Variant<>(getRecipientStrings(getGroup().adminMembers()), "as")), + () -> new Variant<>(getRecipientStrings(getGroup().members() + .stream() + .filter(GroupMember::isAdmin) + .map(GroupMember::recipientAddress) + .collect(Collectors.toSet())), "as")), new DbusProperty<>("Banned", () -> new Variant<>(getRecipientStrings(getGroup().bannedMembers()), "as")), new DbusProperty<>("PermissionAddMember", diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 4b83166b..9a8f7b95 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1476,6 +1476,20 @@ } ] }, + { + "type": "kotlin.Pair", + "jniAccessible": true, + "methods": [ + { + "name": "getFirst", + "parameterTypes": [] + }, + { + "name": "getSecond", + "parameterTypes": [] + } + ] + }, { "type": "kotlin.SafePublicationLazyImpl", "fields": [ @@ -2129,6 +2143,19 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, + { + "type": "org.asamk.signal.commands.ListGroupsCommand$JsonGroupMemberAddress", + "methods": [ + { + "name": "number", + "parameterTypes": [] + }, + { + "name": "uuid", + "parameterTypes": [] + } + ] + }, { "type": "org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity", "allDeclaredFields": true, @@ -2513,6 +2540,10 @@ "name": "internal", "parameterTypes": [] }, + { + "name": "isArchived", + "parameterTypes": [] + }, { "name": "isBlocked", "parameterTypes": [] @@ -5494,6 +5525,18 @@ { "type": "org.signal.core.models.ServiceId$PNI" }, + { + "type": "org.signal.libsignal.attest.AttestationDataException", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "org.signal.libsignal.internal.CompletableFuture", "jniAccessible": true, @@ -5805,6 +5848,78 @@ { "type": "org.signal.libsignal.protocol.SessionCipher$1", "jniAccessible": true, + "methods": [ + { + "name": "getIdentityKey", + "parameterTypes": [ + "long" + ] + }, + { + "name": "getLocalIdentityKeyPair", + "parameterTypes": [] + }, + { + "name": "getLocalRegistrationId", + "parameterTypes": [] + }, + { + "name": "isTrustedIdentity", + "parameterTypes": [ + "long", + "long", + "int" + ] + }, + { + "name": "loadPreKey", + "parameterTypes": [ + "int" + ] + }, + { + "name": "removePreKey", + "parameterTypes": [ + "int" + ] + }, + { + "name": "saveIdentityKey", + "parameterTypes": [ + "long", + "long" + ] + } + ] + }, + { + "type": "org.signal.libsignal.protocol.SessionCipher$2", + "jniAccessible": true, + "methods": [ + { + "name": "loadSession", + "parameterTypes": [ + "long" + ] + }, + { + "name": "loadSignedPreKey", + "parameterTypes": [ + "int" + ] + }, + { + "name": "storeSession", + "parameterTypes": [ + "long", + "long" + ] + } + ] + }, + { + "type": "org.signal.libsignal.protocol.SessionCipher$3", + "jniAccessible": true, "methods": [ { "name": "loadPreKey", @@ -5821,7 +5936,7 @@ ] }, { - "type": "org.signal.libsignal.protocol.SessionCipher$2", + "type": "org.signal.libsignal.protocol.SessionCipher$4", "jniAccessible": true, "methods": [ { @@ -5832,6 +5947,26 @@ } ] }, + { + "type": "org.signal.libsignal.protocol.SessionCipher$5", + "jniAccessible": true, + "methods": [ + { + "name": "loadKyberPreKey", + "parameterTypes": [ + "int" + ] + }, + { + "name": "markKyberPreKeyUsed", + "parameterTypes": [ + "int", + "int", + "long" + ] + } + ] + }, { "type": "org.signal.libsignal.protocol.SignalProtocolAddress", "jniAccessible": true, @@ -5863,6 +5998,9 @@ } ] }, + { + "type": "org.signal.libsignal.protocol.ecc.ECPrivateKey" + }, { "type": "org.signal.libsignal.protocol.ecc.ECPublicKey", "jniAccessible": true, @@ -5887,6 +6025,27 @@ } ] }, + { + "type": "org.signal.libsignal.protocol.groups.GroupCipher$1", + "jniAccessible": true, + "methods": [ + { + "name": "loadSenderKey", + "parameterTypes": [ + "long", + "java.util.UUID" + ] + }, + { + "name": "storeSenderKey", + "parameterTypes": [ + "long", + "java.util.UUID", + "long" + ] + } + ] + }, { "type": "org.signal.libsignal.protocol.groups.state.SenderKeyRecord", "jniAccessible": true, @@ -6078,10 +6237,26 @@ "allDeclaredMethods": true, "jniAccessible": true }, + { + "type": "org.signal.libsignal.protocol.state.internal.IdentityKeyStore", + "jniAccessible": true + }, + { + "type": "org.signal.libsignal.protocol.state.internal.KyberPreKeyStore", + "jniAccessible": true + }, { "type": "org.signal.libsignal.protocol.state.internal.PreKeyStore", "jniAccessible": true }, + { + "type": "org.signal.libsignal.protocol.state.internal.SenderKeyStore", + "jniAccessible": true + }, + { + "type": "org.signal.libsignal.protocol.state.internal.SessionStore", + "jniAccessible": true + }, { "type": "org.signal.libsignal.protocol.state.internal.SignedPreKeyStore", "jniAccessible": true @@ -7922,6 +8097,18 @@ { "type": "org.whispersystems.signalservice.internal.push.GroupMismatchedDevices[]" }, + { + "type": "org.whispersystems.signalservice.internal.push.GroupPatchResponse", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Integer", + "java.lang.String" + ] + } + ] + }, { "type": "org.whispersystems.signalservice.internal.push.GroupStaleDevices", "allDeclaredFields": true,