From 91700ce995ae381dd97b246ea3ff11afb748e421 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 May 2023 12:47:35 +0200 Subject: [PATCH] Implement textStyles for sending and receiving Fixes #1250 --- CHANGELOG.md | 3 + graalvm-config-dir/reflect-config.json | 15 ++++- .../org/asamk/signal/manager/ManagerImpl.java | 6 +- .../org/asamk/signal/manager/api/Message.java | 11 +++- .../signal/manager/api/MessageEnvelope.java | 26 -------- .../asamk/signal/manager/api/TextStyle.java | 61 +++++++++++++++++++ man/signal-cli.1.adoc | 9 +++ .../asamk/signal/ReceiveMessageHandler.java | 3 +- .../asamk/signal/commands/SendCommand.java | 44 ++++++++++++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 9 ++- .../asamk/signal/json/JsonDataMessage.java | 8 ++- .../java/org/asamk/signal/json/JsonQuote.java | 10 ++- .../org/asamk/signal/json/JsonTextStyle.java | 10 +++ 13 files changed, 176 insertions(+), 39 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/TextStyle.java create mode 100644 src/main/java/org/asamk/signal/json/JsonTextStyle.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fe72cadf..6419350e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## [Unreleased] **Attention**: Now requires native libsignal-client version 0.24.0 +### Added +- New `--text-style` and `--quote-text-style` flags for `send` command + ## [0.11.10] - 2023-05-11 **Attention**: Now requires native libsignal-client version 0.23.1 diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 5ace3d39..240edebd 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -838,6 +838,7 @@ {"name":"remoteDelete","parameterTypes":[] }, {"name":"sticker","parameterTypes":[] }, {"name":"storyContext","parameterTypes":[] }, + {"name":"textStyles","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"viewOnce","parameterTypes":[] } ] @@ -940,7 +941,8 @@ {"name":"authorUuid","parameterTypes":[] }, {"name":"id","parameterTypes":[] }, {"name":"mentions","parameterTypes":[] }, - {"name":"text","parameterTypes":[] } + {"name":"text","parameterTypes":[] }, + {"name":"textStyles","parameterTypes":[] } ] }, { @@ -1138,6 +1140,17 @@ {"name":"destinationUuid","parameterTypes":[] } ] }, +{ + "name":"org.asamk.signal.json.JsonTextStyle", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"length","parameterTypes":[] }, + {"name":"start","parameterTypes":[] }, + {"name":"style","parameterTypes":[] } + ] +}, { "name":"org.asamk.signal.json.JsonTypingMessage", "allDeclaredFields":true, diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 1cc0629b..6895da63 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -40,6 +40,7 @@ import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.StickerPackId; import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackUrl; +import org.asamk.signal.manager.api.TextStyle; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; @@ -618,6 +619,9 @@ class ManagerImpl implements Manager { if (message.mentions().size() > 0) { messageBuilder.withMentions(resolveMentions(message.mentions())); } + if (message.textStyles().size() > 0) { + messageBuilder.withBodyRanges(message.textStyles().stream().map(TextStyle::toBodyRange).toList()); + } if (message.quote().isPresent()) { final var quote = message.quote().get(); messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(), @@ -628,7 +632,7 @@ class ManagerImpl implements Manager { List.of(), resolveMentions(quote.mentions()), SignalServiceDataMessage.Quote.Type.NORMAL, - List.of())); + quote.textStyles().stream().map(TextStyle::toBodyRange).toList())); } if (message.sticker().isPresent()) { final var sticker = message.sticker().get(); 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 aba79cc5..ac9e4999 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 @@ -10,12 +10,19 @@ public record Message( Optional quote, Optional sticker, List previews, - Optional storyReply + Optional storyReply, + List textStyles ) { public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} - public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List mentions) {} + public record Quote( + long timestamp, + RecipientIdentifier.Single author, + String message, + List mentions, + List textStyles + ) {} public record Sticker(byte[] packId, int stickerId) {} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index a8f00875..aba52179 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -515,32 +515,6 @@ public record MessageEnvelope( } } - public record TextStyle(Style style, int start, int length) { - - public enum Style { - NONE, - BOLD, - ITALIC, - SPOILER, - STRIKETHROUGH, - MONOSPACE; - - static Style from(BodyRange.Style style) { - return switch (style) { - case NONE -> NONE; - case BOLD -> BOLD; - case ITALIC -> ITALIC; - case SPOILER -> SPOILER; - case STRIKETHROUGH -> STRIKETHROUGH; - case MONOSPACE -> MONOSPACE; - }; - } - } - - static TextStyle from(BodyRange bodyRange) { - return new TextStyle(Style.from(bodyRange.getStyle()), bodyRange.getStart(), bodyRange.getLength()); - } - } } public record Edit(long targetSentTimestamp, Data dataMessage) { diff --git a/lib/src/main/java/org/asamk/signal/manager/api/TextStyle.java b/lib/src/main/java/org/asamk/signal/manager/api/TextStyle.java new file mode 100644 index 00000000..48316308 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/TextStyle.java @@ -0,0 +1,61 @@ +package org.asamk.signal.manager.api; + +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +public record TextStyle(Style style, int start, int length) { + + public enum Style { + NONE, + BOLD, + ITALIC, + SPOILER, + STRIKETHROUGH, + MONOSPACE; + + static Style fromInternal(SignalServiceProtos.BodyRange.Style style) { + return switch (style) { + case NONE -> NONE; + case BOLD -> BOLD; + case ITALIC -> ITALIC; + case SPOILER -> SPOILER; + case STRIKETHROUGH -> STRIKETHROUGH; + case MONOSPACE -> MONOSPACE; + }; + } + + public static Style from(String style) { + return switch (style) { + case "NONE" -> NONE; + case "BOLD" -> BOLD; + case "ITALIC" -> ITALIC; + case "SPOILER" -> SPOILER; + case "STRIKETHROUGH" -> STRIKETHROUGH; + case "MONOSPACE" -> MONOSPACE; + default -> null; + }; + } + + SignalServiceProtos.BodyRange.Style toBodyRangeStyle() { + return switch (this) { + case NONE -> SignalServiceProtos.BodyRange.Style.NONE; + case BOLD -> SignalServiceProtos.BodyRange.Style.BOLD; + case ITALIC -> SignalServiceProtos.BodyRange.Style.ITALIC; + case SPOILER -> SignalServiceProtos.BodyRange.Style.SPOILER; + case STRIKETHROUGH -> SignalServiceProtos.BodyRange.Style.STRIKETHROUGH; + case MONOSPACE -> SignalServiceProtos.BodyRange.Style.MONOSPACE; + }; + } + } + + static TextStyle from(SignalServiceProtos.BodyRange bodyRange) { + return new TextStyle(Style.fromInternal(bodyRange.getStyle()), bodyRange.getStart(), bodyRange.getLength()); + } + + public SignalServiceProtos.BodyRange toBodyRange() { + return SignalServiceProtos.BodyRange.newBuilder() + .setStart(this.start()) + .setLength(this.length()) + .setStyle(this.style().toBodyRangeStyle()) + .build(); + } +} diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 57a10e5a..56787aee 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -251,6 +251,12 @@ e.g.: `--sticker 00abac3bc18d7f599bff2325dc306d43:2` Mention another group member (syntax: start:length:recipientNumber) In the apps the mention replaces part of the message text, which is specified by the start and length values. e.g.: `-m "Hi X!" --mention "3:1:+123456789"` +*--text-style*:: +Style parts of the message text (syntax: start:length:STYLE). +Where STYLE is one of: BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE + +e.g.: `-m "Something BIG!" --mention "10:3:BOLD"` + *--quote-timestamp*:: Specify the timestamp of a previous message with the recipient or group to add a quote to the new message. @@ -263,6 +269,9 @@ Specify the message of the original message. *--quote-mention*:: Specify the mentions of the original message (same format as `--mention`). +*--quote-text-style*:: +Style parts of the original message text (same format as `--text-style`). + *--preview-url*:: Specify the url for the link preview. The same url must also appear in the message body, otherwise the preview won't be displayed by the apps. diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 1f4253ac..c299ae38 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.RecipientAddress; import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.TextStyle; import org.asamk.signal.manager.api.UntrustedIdentityException; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.output.PlainTextWriter; @@ -573,7 +574,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } private void printTextStyle( - PlainTextWriter writer, MessageEnvelope.Data.TextStyle textStyle + PlainTextWriter writer, TextStyle textStyle ) { writer.println("- {}: {} (length: {})", textStyle.style().name(), textStyle.start(), textStyle.length()); } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 9534f182..c1081717 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -12,6 +12,7 @@ import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.TextStyle; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -66,6 +67,9 @@ public class SendCommand implements JsonRpcLocalCommand { subparser.addArgument("--mention") .nargs("*") .help("Mention another group member (syntax: start:length:recipientNumber)"); + subparser.addArgument("--text-style") + .nargs("*") + .help("Style parts of the message text (syntax: start:length:STYLE)"); subparser.addArgument("--quote-timestamp") .type(long.class) .help("Specify the timestamp of a previous message with the recipient or group to add a quote to the new message."); @@ -74,6 +78,9 @@ public class SendCommand implements JsonRpcLocalCommand { subparser.addArgument("--quote-mention") .nargs("*") .help("Quote with mention of another group member (syntax: start:length:recipientNumber)"); + subparser.addArgument("--quote-text-style") + .nargs("*") + .help("Quote with style parts of the message text (syntax: start:length:STYLE)"); subparser.addArgument("--sticker").help("Send a sticker (syntax: stickerPackId:stickerId)"); subparser.addArgument("--preview-url") .help("Specify the url for the link preview (the same url must also appear in the message body)."); @@ -146,6 +153,9 @@ public class SendCommand implements JsonRpcLocalCommand { List mentionStrings = ns.getList("mention"); final var mentions = mentionStrings == null ? List.of() : parseMentions(m, mentionStrings); + List textStyleStrings = ns.getList("text-style"); + final var textStyles = textStyleStrings == null ? List.of() : parseTextStyles(textStyleStrings); + final Message.Quote quote; final var quoteTimestamp = ns.getLong("quote-timestamp"); if (quoteTimestamp != null) { @@ -155,10 +165,15 @@ public class SendCommand implements JsonRpcLocalCommand { final var quoteMentions = quoteMentionStrings == null ? List.of() : parseMentions(m, quoteMentionStrings); + List quoteTextStyleStrings = ns.getList("quote-text-style"); + final var quoteTextStyles = quoteTextStyleStrings == null + ? List.of() + : parseTextStyles(quoteTextStyleStrings); quote = new Message.Quote(quoteTimestamp, CommandUtil.getSingleRecipientIdentifier(quoteAuthor, m.getSelfNumber()), quoteMessage == null ? "" : quoteMessage, - quoteMentions); + quoteMentions, + quoteTextStyles); } else { quote = null; } @@ -201,7 +216,8 @@ public class SendCommand implements JsonRpcLocalCommand { Optional.ofNullable(quote), Optional.ofNullable(sticker), previews, - Optional.ofNullable((storyReply))); + Optional.ofNullable((storyReply)), + textStyles); var results = editTimestamp != null ? m.sendEditMessage(message, recipientIdentifiers, editTimestamp) : m.sendMessage(message, recipientIdentifiers); @@ -237,6 +253,30 @@ public class SendCommand implements JsonRpcLocalCommand { return mentions; } + private List parseTextStyles( + final List textStylesStrings + ) throws UserErrorException { + List textStyles; + final Pattern textStylePattern = Pattern.compile("(\\d+):(\\d+):(.+)"); + textStyles = new ArrayList<>(); + for (final var textStyle : textStylesStrings) { + final var matcher = textStylePattern.matcher(textStyle); + if (!matcher.matches()) { + throw new UserErrorException("Invalid textStyle syntax (" + + textStyle + + ") expected 'start:length:STYLE'"); + } + final var style = TextStyle.Style.from(matcher.group(3)); + if (style == null) { + throw new UserErrorException("Invalid style: " + matcher.group(3)); + } + textStyles.add(new TextStyle(style, + Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)))); + } + return textStyles; + } + private Message.Sticker parseSticker(final String stickerString) throws UserErrorException { final Pattern stickerPattern = Pattern.compile("([\\da-f]+):(\\d+)"); final var matcher = stickerPattern.matcher(stickerString); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index dafcabab..f91f8001 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -219,7 +219,8 @@ public class DbusSignalImpl implements Signal { Optional.empty(), Optional.empty(), List.of(), - Optional.empty()), + Optional.empty(), + List.of()), getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); @@ -388,7 +389,8 @@ public class DbusSignalImpl implements Signal { Optional.empty(), Optional.empty(), List.of(), - Optional.empty()), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); + Optional.empty(), + List.of()), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); checkSendMessageResults(results); return results.timestamp(); } catch (AttachmentInvalidException e) { @@ -431,7 +433,8 @@ public class DbusSignalImpl implements Signal { Optional.empty(), Optional.empty(), List.of(), - Optional.empty()), Set.of(getGroupRecipientIdentifier(groupId))); + Optional.empty(), + List.of()), Set.of(getGroupRecipientIdentifier(groupId))); checkSendMessageResults(results); return results.timestamp(); } catch (IOException | InvalidStickerException e) { diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index 5511f531..6da7145a 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -20,6 +20,7 @@ record JsonDataMessage( @JsonInclude(JsonInclude.Include.NON_NULL) JsonSticker sticker, @JsonInclude(JsonInclude.Include.NON_NULL) JsonRemoteDelete remoteDelete, @JsonInclude(JsonInclude.Include.NON_NULL) List contacts, + @JsonInclude(JsonInclude.Include.NON_NULL) List textStyles, @JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupInfo groupInfo, @JsonInclude(JsonInclude.Include.NON_NULL) JsonStoryContext storyContext ) { @@ -53,11 +54,15 @@ record JsonDataMessage( .map(JsonAttachment::from) .toList() : null; final var sticker = dataMessage.sticker().isPresent() ? JsonSticker.from(dataMessage.sticker().get()) : null; - final var contacts = dataMessage.sharedContacts().size() > 0 ? dataMessage.sharedContacts() .stream() .map(JsonSharedContact::from) .toList() : null; + final var textStyles = dataMessage.textStyles().size() > 0 ? dataMessage.textStyles() + .stream() + .map(JsonTextStyle::from) + .toList() : null; + return new JsonDataMessage(timestamp, message, expiresInSeconds, @@ -71,6 +76,7 @@ record JsonDataMessage( sticker, remoteDelete, contacts, + textStyles, groupInfo, storyContext); } diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java index 01e35eb7..94f3f52c 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuote.java +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -14,7 +14,8 @@ public record JsonQuote( String authorUuid, String text, @JsonInclude(JsonInclude.Include.NON_NULL) List mentions, - List attachments + List attachments, + @JsonInclude(JsonInclude.Include.NON_NULL) List textStyles ) { static JsonQuote from(MessageEnvelope.Data.Quote quote) { @@ -34,6 +35,11 @@ public record JsonQuote( .map(JsonQuotedAttachment::from) .toList() : List.of(); - return new JsonQuote(id, author, authorNumber, authorUuid, text, mentions, attachments); + final var textStyles = quote.textStyles().size() > 0 ? quote.textStyles() + .stream() + .map(JsonTextStyle::from) + .toList() : null; + + return new JsonQuote(id, author, authorNumber, authorUuid, text, mentions, attachments, textStyles); } } diff --git a/src/main/java/org/asamk/signal/json/JsonTextStyle.java b/src/main/java/org/asamk/signal/json/JsonTextStyle.java new file mode 100644 index 00000000..898e7db6 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonTextStyle.java @@ -0,0 +1,10 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.api.TextStyle; + +public record JsonTextStyle(String style, int start, int length) { + + static JsonTextStyle from(TextStyle textStyle) { + return new JsonTextStyle(textStyle.style().name(), textStyle.start(), textStyle.length()); + } +} -- 2.50.1