Implement textStyles for sending and receiving

Fixes #1250
This commit is contained in:
AsamK 2023-05-20 12:47:35 +02:00
parent 145a2f1179
commit 91700ce995
13 changed files with 176 additions and 39 deletions

View file

@ -3,6 +3,9 @@
## [Unreleased] ## [Unreleased]
**Attention**: Now requires native libsignal-client version 0.24.0 **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 ## [0.11.10] - 2023-05-11
**Attention**: Now requires native libsignal-client version 0.23.1 **Attention**: Now requires native libsignal-client version 0.23.1

View file

@ -838,6 +838,7 @@
{"name":"remoteDelete","parameterTypes":[] }, {"name":"remoteDelete","parameterTypes":[] },
{"name":"sticker","parameterTypes":[] }, {"name":"sticker","parameterTypes":[] },
{"name":"storyContext","parameterTypes":[] }, {"name":"storyContext","parameterTypes":[] },
{"name":"textStyles","parameterTypes":[] },
{"name":"timestamp","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] },
{"name":"viewOnce","parameterTypes":[] } {"name":"viewOnce","parameterTypes":[] }
] ]
@ -940,7 +941,8 @@
{"name":"authorUuid","parameterTypes":[] }, {"name":"authorUuid","parameterTypes":[] },
{"name":"id","parameterTypes":[] }, {"name":"id","parameterTypes":[] },
{"name":"mentions","parameterTypes":[] }, {"name":"mentions","parameterTypes":[] },
{"name":"text","parameterTypes":[] } {"name":"text","parameterTypes":[] },
{"name":"textStyles","parameterTypes":[] }
] ]
}, },
{ {
@ -1138,6 +1140,17 @@
{"name":"destinationUuid","parameterTypes":[] } {"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", "name":"org.asamk.signal.json.JsonTypingMessage",
"allDeclaredFields":true, "allDeclaredFields":true,

View file

@ -40,6 +40,7 @@ import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.StickerPackId; import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl; 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.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.api.UpdateGroup;
@ -618,6 +619,9 @@ class ManagerImpl implements Manager {
if (message.mentions().size() > 0) { if (message.mentions().size() > 0) {
messageBuilder.withMentions(resolveMentions(message.mentions())); messageBuilder.withMentions(resolveMentions(message.mentions()));
} }
if (message.textStyles().size() > 0) {
messageBuilder.withBodyRanges(message.textStyles().stream().map(TextStyle::toBodyRange).toList());
}
if (message.quote().isPresent()) { if (message.quote().isPresent()) {
final var quote = message.quote().get(); final var quote = message.quote().get();
messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(), messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
@ -628,7 +632,7 @@ class ManagerImpl implements Manager {
List.of(), List.of(),
resolveMentions(quote.mentions()), resolveMentions(quote.mentions()),
SignalServiceDataMessage.Quote.Type.NORMAL, SignalServiceDataMessage.Quote.Type.NORMAL,
List.of())); quote.textStyles().stream().map(TextStyle::toBodyRange).toList()));
} }
if (message.sticker().isPresent()) { if (message.sticker().isPresent()) {
final var sticker = message.sticker().get(); final var sticker = message.sticker().get();

View file

@ -10,12 +10,19 @@ public record Message(
Optional<Quote> quote, Optional<Quote> quote,
Optional<Sticker> sticker, Optional<Sticker> sticker,
List<Preview> previews, List<Preview> previews,
Optional<StoryReply> storyReply Optional<StoryReply> storyReply,
List<TextStyle> textStyles
) { ) {
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List<Mention> mentions) {} public record Quote(
long timestamp,
RecipientIdentifier.Single author,
String message,
List<Mention> mentions,
List<TextStyle> textStyles
) {}
public record Sticker(byte[] packId, int stickerId) {} public record Sticker(byte[] packId, int stickerId) {}

View file

@ -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) { public record Edit(long targetSentTimestamp, Data dataMessage) {

View file

@ -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();
}
}

View file

@ -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. 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"` 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*:: *--quote-timestamp*::
Specify the timestamp of a previous message with the recipient or group to add a quote to the new message. 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*:: *--quote-mention*::
Specify the mentions of the original message (same format as `--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*:: *--preview-url*::
Specify the url for the link preview. 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. The same url must also appear in the message body, otherwise the preview won't be displayed by the apps.

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.RecipientAddress; import org.asamk.signal.manager.api.RecipientAddress;
import org.asamk.signal.manager.api.RecipientIdentifier; 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.api.UntrustedIdentityException;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.output.PlainTextWriter; import org.asamk.signal.output.PlainTextWriter;
@ -573,7 +574,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
} }
private void printTextStyle( private void printTextStyle(
PlainTextWriter writer, MessageEnvelope.Data.TextStyle textStyle PlainTextWriter writer, TextStyle textStyle
) { ) {
writer.println("- {}: {} (length: {})", textStyle.style().name(), textStyle.start(), textStyle.length()); writer.println("- {}: {} (length: {})", textStyle.style().name(), textStyle.start(), textStyle.length());
} }

View file

@ -12,6 +12,7 @@ import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.RecipientIdentifier; 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.api.UnregisteredRecipientException;
import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
@ -66,6 +67,9 @@ public class SendCommand implements JsonRpcLocalCommand {
subparser.addArgument("--mention") subparser.addArgument("--mention")
.nargs("*") .nargs("*")
.help("Mention another group member (syntax: start:length:recipientNumber)"); .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") subparser.addArgument("--quote-timestamp")
.type(long.class) .type(long.class)
.help("Specify the timestamp of a previous message with the recipient or group to add a quote to the new message."); .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") subparser.addArgument("--quote-mention")
.nargs("*") .nargs("*")
.help("Quote with mention of another group member (syntax: start:length:recipientNumber)"); .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("--sticker").help("Send a sticker (syntax: stickerPackId:stickerId)");
subparser.addArgument("--preview-url") subparser.addArgument("--preview-url")
.help("Specify the url for the link preview (the same url must also appear in the message body)."); .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<String> mentionStrings = ns.getList("mention"); List<String> mentionStrings = ns.getList("mention");
final var mentions = mentionStrings == null ? List.<Message.Mention>of() : parseMentions(m, mentionStrings); final var mentions = mentionStrings == null ? List.<Message.Mention>of() : parseMentions(m, mentionStrings);
List<String> textStyleStrings = ns.getList("text-style");
final var textStyles = textStyleStrings == null ? List.<TextStyle>of() : parseTextStyles(textStyleStrings);
final Message.Quote quote; final Message.Quote quote;
final var quoteTimestamp = ns.getLong("quote-timestamp"); final var quoteTimestamp = ns.getLong("quote-timestamp");
if (quoteTimestamp != null) { if (quoteTimestamp != null) {
@ -155,10 +165,15 @@ public class SendCommand implements JsonRpcLocalCommand {
final var quoteMentions = quoteMentionStrings == null final var quoteMentions = quoteMentionStrings == null
? List.<Message.Mention>of() ? List.<Message.Mention>of()
: parseMentions(m, quoteMentionStrings); : parseMentions(m, quoteMentionStrings);
List<String> quoteTextStyleStrings = ns.getList("quote-text-style");
final var quoteTextStyles = quoteTextStyleStrings == null
? List.<TextStyle>of()
: parseTextStyles(quoteTextStyleStrings);
quote = new Message.Quote(quoteTimestamp, quote = new Message.Quote(quoteTimestamp,
CommandUtil.getSingleRecipientIdentifier(quoteAuthor, m.getSelfNumber()), CommandUtil.getSingleRecipientIdentifier(quoteAuthor, m.getSelfNumber()),
quoteMessage == null ? "" : quoteMessage, quoteMessage == null ? "" : quoteMessage,
quoteMentions); quoteMentions,
quoteTextStyles);
} else { } else {
quote = null; quote = null;
} }
@ -201,7 +216,8 @@ public class SendCommand implements JsonRpcLocalCommand {
Optional.ofNullable(quote), Optional.ofNullable(quote),
Optional.ofNullable(sticker), Optional.ofNullable(sticker),
previews, previews,
Optional.ofNullable((storyReply))); Optional.ofNullable((storyReply)),
textStyles);
var results = editTimestamp != null var results = editTimestamp != null
? m.sendEditMessage(message, recipientIdentifiers, editTimestamp) ? m.sendEditMessage(message, recipientIdentifiers, editTimestamp)
: m.sendMessage(message, recipientIdentifiers); : m.sendMessage(message, recipientIdentifiers);
@ -237,6 +253,30 @@ public class SendCommand implements JsonRpcLocalCommand {
return mentions; return mentions;
} }
private List<TextStyle> parseTextStyles(
final List<String> textStylesStrings
) throws UserErrorException {
List<TextStyle> 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 { private Message.Sticker parseSticker(final String stickerString) throws UserErrorException {
final Pattern stickerPattern = Pattern.compile("([\\da-f]+):(\\d+)"); final Pattern stickerPattern = Pattern.compile("([\\da-f]+):(\\d+)");
final var matcher = stickerPattern.matcher(stickerString); final var matcher = stickerPattern.matcher(stickerString);

View file

@ -219,7 +219,8 @@ public class DbusSignalImpl implements Signal {
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
List.of(), List.of(),
Optional.empty()), Optional.empty(),
List.of()),
getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast) .map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
@ -388,7 +389,8 @@ public class DbusSignalImpl implements Signal {
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
List.of(), List.of(),
Optional.empty()), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); Optional.empty(),
List.of()), Set.of(RecipientIdentifier.NoteToSelf.INSTANCE));
checkSendMessageResults(results); checkSendMessageResults(results);
return results.timestamp(); return results.timestamp();
} catch (AttachmentInvalidException e) { } catch (AttachmentInvalidException e) {
@ -431,7 +433,8 @@ public class DbusSignalImpl implements Signal {
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
List.of(), List.of(),
Optional.empty()), Set.of(getGroupRecipientIdentifier(groupId))); Optional.empty(),
List.of()), Set.of(getGroupRecipientIdentifier(groupId)));
checkSendMessageResults(results); checkSendMessageResults(results);
return results.timestamp(); return results.timestamp();
} catch (IOException | InvalidStickerException e) { } catch (IOException | InvalidStickerException e) {

View file

@ -20,6 +20,7 @@ record JsonDataMessage(
@JsonInclude(JsonInclude.Include.NON_NULL) JsonSticker sticker, @JsonInclude(JsonInclude.Include.NON_NULL) JsonSticker sticker,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonRemoteDelete remoteDelete, @JsonInclude(JsonInclude.Include.NON_NULL) JsonRemoteDelete remoteDelete,
@JsonInclude(JsonInclude.Include.NON_NULL) List<JsonSharedContact> contacts, @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonSharedContact> contacts,
@JsonInclude(JsonInclude.Include.NON_NULL) List<JsonTextStyle> textStyles,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupInfo groupInfo, @JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupInfo groupInfo,
@JsonInclude(JsonInclude.Include.NON_NULL) JsonStoryContext storyContext @JsonInclude(JsonInclude.Include.NON_NULL) JsonStoryContext storyContext
) { ) {
@ -53,11 +54,15 @@ record JsonDataMessage(
.map(JsonAttachment::from) .map(JsonAttachment::from)
.toList() : null; .toList() : null;
final var sticker = dataMessage.sticker().isPresent() ? JsonSticker.from(dataMessage.sticker().get()) : null; final var sticker = dataMessage.sticker().isPresent() ? JsonSticker.from(dataMessage.sticker().get()) : null;
final var contacts = dataMessage.sharedContacts().size() > 0 ? dataMessage.sharedContacts() final var contacts = dataMessage.sharedContacts().size() > 0 ? dataMessage.sharedContacts()
.stream() .stream()
.map(JsonSharedContact::from) .map(JsonSharedContact::from)
.toList() : null; .toList() : null;
final var textStyles = dataMessage.textStyles().size() > 0 ? dataMessage.textStyles()
.stream()
.map(JsonTextStyle::from)
.toList() : null;
return new JsonDataMessage(timestamp, return new JsonDataMessage(timestamp,
message, message,
expiresInSeconds, expiresInSeconds,
@ -71,6 +76,7 @@ record JsonDataMessage(
sticker, sticker,
remoteDelete, remoteDelete,
contacts, contacts,
textStyles,
groupInfo, groupInfo,
storyContext); storyContext);
} }

View file

@ -14,7 +14,8 @@ public record JsonQuote(
String authorUuid, String authorUuid,
String text, String text,
@JsonInclude(JsonInclude.Include.NON_NULL) List<JsonMention> mentions, @JsonInclude(JsonInclude.Include.NON_NULL) List<JsonMention> mentions,
List<JsonQuotedAttachment> attachments List<JsonQuotedAttachment> attachments,
@JsonInclude(JsonInclude.Include.NON_NULL) List<JsonTextStyle> textStyles
) { ) {
static JsonQuote from(MessageEnvelope.Data.Quote quote) { static JsonQuote from(MessageEnvelope.Data.Quote quote) {
@ -34,6 +35,11 @@ public record JsonQuote(
.map(JsonQuotedAttachment::from) .map(JsonQuotedAttachment::from)
.toList() : List.<JsonQuotedAttachment>of(); .toList() : List.<JsonQuotedAttachment>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);
} }
} }

View file

@ -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());
}
}