Implement Stories feature behind flag.

Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com>
Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
Alex Hart
2022-02-24 13:40:28 -04:00
parent 765185952e
commit 174cd860a0
416 changed files with 19506 additions and 857 deletions

View File

@@ -41,7 +41,10 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
@@ -102,8 +105,11 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMe
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Preview;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.StoryMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TextAttachment;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
import org.whispersystems.signalservice.internal.push.StaleDevices;
@@ -219,22 +225,8 @@ public class SignalServiceMessageSender {
}
/**
* Send a typing indicator.
*
* @param recipient The destination
* @param message The typing indicator to deliver
* Sends a typing indicator using client-side fanout. Doesn't bother with return results, since these are best-effort.
*/
public void sendTyping(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceTypingMessage message)
throws IOException, UntrustedIdentityException
{
Content content = createTypingContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null);
}
public void sendTyping(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SignalServiceTypingMessage message,
@@ -248,7 +240,7 @@ public class SignalServiceMessageSender {
}
/**
* Send a typing indicator a group. Doesn't bother with return results, since these are best-effort.
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
*/
public void sendGroupTyping(DistributionId distributionId,
List<SignalServiceAddress> recipients,
@@ -257,7 +249,35 @@ public class SignalServiceMessageSender {
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{
Content content = createTypingContent(message);
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId().orNull(), true, SenderKeyGroupEvents.EMPTY);
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY);
}
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SignalServiceStoryMessage message,
long timestamp)
throws IOException, UntrustedIdentityException
{
Content content = createStoryContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.absent());
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null);
}
/**
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
* @return
*/
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
Optional<byte[]> groupId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
SignalServiceStoryMessage message,
long timestamp)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{
Content content = createStoryContent(message);
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY);
}
@@ -297,7 +317,7 @@ public class SignalServiceMessageSender {
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{
Content content = createCallContent(message);
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId().get(), false, SenderKeyGroupEvents.EMPTY);
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY);
}
/**
@@ -369,12 +389,12 @@ public class SignalServiceMessageSender {
List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SenderKeyDistributionMessage message,
byte[] groupId)
Optional<byte[]> groupId)
throws IOException
{
ByteString distributionBytes = ByteString.copyFrom(message.serialize());
Content content = Content.newBuilder().setSenderKeyDistributionMessage(distributionBytes).build();
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.of(groupId));
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, groupId);
long timestamp = System.currentTimeMillis();
Log.d(TAG, "[" + timestamp + "] Sending SKDM to " + recipients.size() + " recipients for DistributionId " + distributionId);
@@ -421,7 +441,7 @@ public class SignalServiceMessageSender {
Content content = createMessageContent(message);
Optional<byte[]> groupId = message.getGroupId();
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId.orNull(), false, sendEvents);
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents);
sendEvents.onMessageSent();
@@ -725,6 +745,33 @@ public class SignalServiceMessageSender {
return container.setTypingMessage(builder).build();
}
private Content createStoryContent(SignalServiceStoryMessage message) throws IOException {
Content.Builder container = Content.newBuilder();
StoryMessage.Builder builder = StoryMessage.newBuilder();
if (message.getProfileKey().isPresent()) {
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
}
if (message.getGroupContext().isPresent()) {
builder.setGroup(createGroupContent(message.getGroupContext().get()));
}
if (message.getFileAttachment().isPresent()) {
if (message.getFileAttachment().get().isStream()) {
builder.setFileAttachment(createAttachmentPointer(message.getFileAttachment().get().asStream()));
} else {
builder.setFileAttachment(createAttachmentPointer(message.getFileAttachment().get().asPointer()));
}
}
if (message.getTextAttachment().isPresent()) {
builder.setTextAttachment(createTextAttachment(message.getTextAttachment().get()));
}
return container.setStoryMessage(builder).build();
}
private Content createReceiptContent(SignalServiceReceiptMessage message) {
Content.Builder container = Content.newBuilder();
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
@@ -832,22 +879,8 @@ public class SignalServiceMessageSender {
}
if (message.getPreviews().isPresent()) {
for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) {
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
previewBuilder.setTitle(preview.getTitle());
previewBuilder.setDescription(preview.getDescription());
previewBuilder.setDate(preview.getDate());
previewBuilder.setUrl(preview.getUrl());
if (preview.getImage().isPresent()) {
if (preview.getImage().get().isStream()) {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream()));
} else {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer()));
}
}
builder.addPreview(previewBuilder.build());
for (SignalServicePreview preview : message.getPreviews().get()) {
builder.addPreview(createPreview(preview));
}
}
@@ -936,6 +969,24 @@ public class SignalServiceMessageSender {
return enforceMaxContentSize(container.setDataMessage(builder).build());
}
private Preview createPreview(SignalServicePreview preview) throws IOException {
Preview.Builder previewBuilder = Preview.newBuilder()
.setTitle(preview.getTitle())
.setDescription(preview.getDescription())
.setDate(preview.getDate())
.setUrl(preview.getUrl());
if (preview.getImage().isPresent()) {
if (preview.getImage().get().isStream()) {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream()));
} else {
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer()));
}
}
return previewBuilder.build();
}
private Content createCallContent(SignalServiceCallMessage callMessage) {
Content.Builder container = Content.newBuilder();
CallMessage.Builder builder = CallMessage.newBuilder();
@@ -1700,7 +1751,7 @@ public class SignalServiceMessageSender {
long timestamp,
Content content,
ContentHint contentHint,
byte[] groupId,
Optional<byte[]> groupId,
boolean online,
SenderKeyGroupEvents sendEvents)
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException
@@ -1922,6 +1973,54 @@ public class SignalServiceMessageSender {
return createAttachmentPointer(uploadAttachment(attachment));
}
private TextAttachment createTextAttachment(SignalServiceTextAttachment attachment) throws IOException {
TextAttachment.Builder builder = TextAttachment.newBuilder();
if (attachment.getStyle().isPresent()) {
switch (attachment.getStyle().get()) {
case DEFAULT:
builder.setTextStyle(TextAttachment.Style.DEFAULT);
break;
case REGULAR:
builder.setTextStyle(TextAttachment.Style.REGULAR);
break;
case BOLD:
builder.setTextStyle(TextAttachment.Style.BOLD);
break;
case SERIF:
builder.setTextStyle(TextAttachment.Style.SERIF);
break;
case SCRIPT:
builder.setTextStyle(TextAttachment.Style.SCRIPT);
break;
case CONDENSED:
builder.setTextStyle(TextAttachment.Style.CONDENSED);
break;
default:
throw new AssertionError("Unknown type: " + attachment.getStyle().get());
}
}
TextAttachment.Gradient.Builder gradientBuilder = TextAttachment.Gradient.newBuilder();
if (attachment.getBackgroundGradient().isPresent()) {
SignalServiceTextAttachment.Gradient gradient = attachment.getBackgroundGradient().get();
if (gradient.getStartColor().isPresent()) gradientBuilder.setStartColor(gradient.getStartColor().get());
if (gradient.getEndColor().isPresent()) gradientBuilder.setEndColor(gradient.getEndColor().get());
if (gradient.getAngle().isPresent()) gradientBuilder.setAngle(gradient.getAngle().get());
builder.setGradient(gradientBuilder.build());
}
if (attachment.getText().isPresent()) builder.setText(attachment.getText().get());
if (attachment.getTextForegroundColor().isPresent()) builder.setTextForegroundColor(attachment.getTextForegroundColor().get());
if (attachment.getTextBackgroundColor().isPresent()) builder.setTextBackgroundColor(attachment.getTextBackgroundColor().get());
if (attachment.getPreview().isPresent()) builder.setPreview(createPreview(attachment.getPreview().get()));
if (attachment.getBackgroundColor().isPresent()) builder.setColor(attachment.getBackgroundColor().get());
return builder.build();
}
private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
SignalServiceAddress recipient,

View File

@@ -144,10 +144,13 @@ public class AccountAttributes {
@JsonProperty
private boolean changeNumber;
@JsonProperty
private boolean stories;
@JsonCreator
public Capabilities() {}
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber) {
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
@@ -155,6 +158,7 @@ public class AccountAttributes {
this.senderKey = senderKey;
this.announcementGroup = announcementGroup;
this.changeNumber = changeNumber;
this.stories = stories;
}
public boolean isUuid() {
@@ -184,5 +188,9 @@ public class AccountAttributes {
public boolean isChangeNumber() {
return changeNumber;
}
public boolean isStories() {
return stories;
}
}
}

View File

@@ -94,7 +94,7 @@ public class SignalServiceCipher {
SenderCertificate senderCertificate,
byte[] unpaddedMessage,
ContentHint contentHint,
byte[] groupId)
Optional<byte[]> groupId)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, InvalidRegistrationIdException
{
PushTransportDetails transport = new PushTransportDetails();
@@ -105,7 +105,7 @@ public class SignalServiceCipher {
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message,
senderCertificate,
contentHint.getType(),
Optional.of(groupId));
groupId);
return sessionCipher.multiRecipientEncrypt(destinations, messageContent);
}

View File

@@ -84,6 +84,7 @@ public final class SignalServiceContent {
private final Optional<SignalServiceTypingMessage> typingMessage;
private final Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage;
private final Optional<DecryptionErrorMessage> decryptionErrorMessage;
private final Optional<SignalServiceStoryMessage> storyMessage;
private SignalServiceContent(SignalServiceDataMessage message,
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
@@ -114,6 +115,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage,
@@ -145,6 +147,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SignalServiceCallMessage callMessage,
@@ -176,6 +179,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SignalServiceReceiptMessage receiptMessage,
@@ -207,6 +211,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(DecryptionErrorMessage errorMessage,
@@ -238,6 +243,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.of(errorMessage);
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SignalServiceTypingMessage typingMessage,
@@ -269,6 +275,7 @@ public final class SignalServiceContent {
this.typingMessage = Optional.of(typingMessage);
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SenderKeyDistributionMessage senderKeyDistributionMessage,
@@ -299,6 +306,38 @@ public final class SignalServiceContent {
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = Optional.of(senderKeyDistributionMessage);
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.absent();
}
private SignalServiceContent(SignalServiceStoryMessage storyMessage,
SignalServiceAddress sender,
int senderDevice,
long timestamp,
long serverReceivedTimestamp,
long serverDeliveredTimestamp,
boolean needsReceipt,
String serverUuid,
Optional<byte[]> groupId,
SignalServiceContentProto serializedState)
{
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.serverReceivedTimestamp = serverReceivedTimestamp;
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
this.needsReceipt = needsReceipt;
this.serverUuid = serverUuid;
this.groupId = groupId;
this.serializedState = serializedState;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
this.senderKeyDistributionMessage = Optional.absent();
this.decryptionErrorMessage = Optional.absent();
this.storyMessage = Optional.of(storyMessage);
}
public Optional<SignalServiceDataMessage> getDataMessage() {
@@ -321,6 +360,10 @@ public final class SignalServiceContent {
return typingMessage;
}
public Optional<SignalServiceStoryMessage> getStoryMessage() {
return storyMessage;
}
public Optional<SenderKeyDistributionMessage> getSenderKeyDistributionMessage() {
return senderKeyDistributionMessage;
}
@@ -496,6 +539,17 @@ public final class SignalServiceContent {
metadata.getServerGuid(),
metadata.getGroupId(),
serviceContentProto);
} else if (message.hasStoryMessage()) {
return new SignalServiceContent(createStoryMessage(message.getStoryMessage()),
metadata.getSender(),
metadata.getSenderDevice(),
metadata.getTimestamp(),
metadata.getServerReceivedTimestamp(),
metadata.getServerDeliveredTimestamp(),
false,
metadata.getServerGuid(),
metadata.getGroupId(),
serviceContentProto);
}
}
@@ -523,7 +577,7 @@ public final class SignalServiceContent {
boolean isGroupV2 = groupInfoV2 != null;
SignalServiceDataMessage.Quote quote = createQuote(content, isGroupV2);
List<SharedContact> sharedContacts = createSharedContacts(content);
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
List<SignalServicePreview> previews = createPreviews(content);
List<SignalServiceDataMessage.Mention> mentions = createMentions(content.getBodyRangesList(), content.getBody(), isGroupV2);
SignalServiceDataMessage.Sticker sticker = createSticker(content);
SignalServiceDataMessage.Reaction reaction = createReaction(content);
@@ -898,6 +952,20 @@ public final class SignalServiceContent {
Optional.absent());
}
private static SignalServiceStoryMessage createStoryMessage(SignalServiceProtos.StoryMessage content) throws InvalidMessageStructureException {
byte[] profileKey = content.hasProfileKey() ? content.getProfileKey().toByteArray() : null;
if (content.hasFileAttachment()) {
return SignalServiceStoryMessage.forFileAttachment(profileKey,
createGroupV2Info(content),
createAttachmentPointer(content.getFileAttachment()));
} else {
return SignalServiceStoryMessage.forTextAttachment(profileKey,
createGroupV2Info(content),
createTextAttachment(content.getTextAttachment()));
}
}
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content, boolean isGroupV2)
throws InvalidMessageStructureException
{
@@ -925,28 +993,32 @@ public final class SignalServiceContent {
}
}
private static List<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
private static List<SignalServicePreview> createPreviews(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
if (content.getPreviewCount() <= 0) return null;
List<SignalServiceDataMessage.Preview> results = new LinkedList<>();
List<SignalServicePreview> results = new LinkedList<>();
for (SignalServiceProtos.DataMessage.Preview preview : content.getPreviewList()) {
SignalServiceAttachment attachment = null;
if (preview.hasImage()) {
attachment = createAttachmentPointer(preview.getImage());
}
results.add(new SignalServiceDataMessage.Preview(preview.getUrl(),
preview.getTitle(),
preview.getDescription(),
preview.getDate(),
Optional.fromNullable(attachment)));
for (SignalServiceProtos.Preview preview : content.getPreviewList()) {
results.add(createPreview(preview));
}
return results;
}
private static SignalServicePreview createPreview(SignalServiceProtos.Preview preview) throws InvalidMessageStructureException {
SignalServiceAttachment attachment = null;
if (preview.hasImage()) {
attachment = createAttachmentPointer(preview.getImage());
}
return new SignalServicePreview(preview.getUrl(),
preview.getTitle(),
preview.getDescription(),
preview.getDate(),
Optional.fromNullable(attachment));
}
private static List<SignalServiceDataMessage.Mention> createMentions(List<SignalServiceProtos.DataMessage.BodyRange> bodyRanges, String body, boolean isGroupV2)
throws InvalidMessageStructureException
{
@@ -1173,13 +1245,73 @@ public final class SignalServiceContent {
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws InvalidMessageStructureException {
return AttachmentPointerUtil.createSignalAttachmentPointer(pointer);
}
private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
if (!content.hasGroupV2()) return null;
private static SignalServiceTextAttachment createTextAttachment(SignalServiceProtos.TextAttachment attachment) throws InvalidMessageStructureException {
SignalServiceTextAttachment.Style style = null;
if (attachment.hasTextStyle()) {
switch (attachment.getTextStyle()) {
case DEFAULT:
style = SignalServiceTextAttachment.Style.DEFAULT;
break;
case REGULAR:
style = SignalServiceTextAttachment.Style.REGULAR;
break;
case BOLD:
style = SignalServiceTextAttachment.Style.BOLD;
break;
case SERIF:
style = SignalServiceTextAttachment.Style.SERIF;
break;
case SCRIPT:
style = SignalServiceTextAttachment.Style.SCRIPT;
break;
case CONDENSED:
style = SignalServiceTextAttachment.Style.CONDENSED;
break;
}
}
Optional<String> text = Optional.fromNullable(attachment.hasText() ? attachment.getText() : null);
Optional<Integer> textForegroundColor = Optional.fromNullable(attachment.hasTextForegroundColor() ? attachment.getTextForegroundColor() : null);
Optional<Integer> textBackgroundColor = Optional.fromNullable(attachment.hasTextBackgroundColor() ? attachment.getTextBackgroundColor() : null);
Optional<SignalServicePreview> preview = Optional.fromNullable(attachment.hasPreview() ? createPreview(attachment.getPreview()) : null);
if (attachment.hasGradient()) {
SignalServiceProtos.TextAttachment.Gradient attachmentGradient = attachment.getGradient();
Integer startColor = attachmentGradient.hasStartColor() ? attachmentGradient.getStartColor() : null;
Integer endColor = attachmentGradient.hasEndColor() ? attachmentGradient.getEndColor() : null;
Integer angle = attachmentGradient.hasAngle() ? attachmentGradient.getAngle() : null;
SignalServiceTextAttachment.Gradient gradient = new SignalServiceTextAttachment.Gradient(Optional.fromNullable(startColor),
Optional.fromNullable(endColor),
Optional.fromNullable(angle));
return SignalServiceTextAttachment.forGradientBackground(text, Optional.fromNullable(style), textForegroundColor, textBackgroundColor, preview, gradient);
} else {
return SignalServiceTextAttachment.forSolidBackground(text, Optional.fromNullable(style), textForegroundColor, textBackgroundColor, preview, attachment.getColor());
}
}
private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.StoryMessage storyMessage) throws InvalidMessageStructureException {
if (!storyMessage.hasGroup()) {
return null;
}
return createGroupV2Info(storyMessage.getGroup());
}
private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage dataMessage) throws InvalidMessageStructureException {
if (!dataMessage.hasGroupV2()) {
return null;
}
return createGroupV2Info(dataMessage.getGroupV2());
}
private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.GroupContextV2 groupV2) throws InvalidMessageStructureException {
if (groupV2 == null) {
return null;
}
SignalServiceProtos.GroupContextV2 groupV2 = content.getGroupV2();
if (!groupV2.hasMasterKey()) {
throw new InvalidMessageStructureException("No GV2 master key on message");
}

View File

@@ -33,7 +33,7 @@ public class SignalServiceDataMessage {
private final boolean profileKeyUpdate;
private final Optional<Quote> quote;
private final Optional<List<SharedContact>> contacts;
private final Optional<List<Preview>> previews;
private final Optional<List<SignalServicePreview>> previews;
private final Optional<List<Mention>> mentions;
private final Optional<Sticker> sticker;
private final boolean viewOnce;
@@ -66,7 +66,7 @@ public class SignalServiceDataMessage {
boolean profileKeyUpdate,
Quote quote,
List<SharedContact> sharedContacts,
List<Preview> previews,
List<SignalServicePreview> previews,
List<Mention> mentions,
Sticker sticker,
boolean viewOnce,
@@ -217,7 +217,7 @@ public class SignalServiceDataMessage {
return contacts;
}
public Optional<List<Preview>> getPreviews() {
public Optional<List<SignalServicePreview>> getPreviews() {
return previews;
}
@@ -271,7 +271,7 @@ public class SignalServiceDataMessage {
private List<SignalServiceAttachment> attachments = new LinkedList<>();
private List<SharedContact> sharedContacts = new LinkedList<>();
private List<Preview> previews = new LinkedList<>();
private List<SignalServicePreview> previews = new LinkedList<>();
private List<Mention> mentions = new LinkedList<>();
private long timestamp;
@@ -378,7 +378,7 @@ public class SignalServiceDataMessage {
return this;
}
public Builder withPreviews(List<Preview> previews) {
public Builder withPreviews(List<SignalServicePreview> previews) {
this.previews.addAll(previews);
return this;
}
@@ -495,42 +495,6 @@ public class SignalServiceDataMessage {
}
}
public static class Preview {
private final String url;
private final String title;
private final String description;
private final long date;
private final Optional<SignalServiceAttachment> image;
public Preview(String url, String title, String description, long date, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.description = description;
this.date = date;
this.image = image;
}
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public long getDate() {
return date;
}
public Optional<SignalServiceAttachment> getImage() {
return image;
}
}
public static class Sticker {
private final byte[] packId;
private final byte[] packKey;

View File

@@ -0,0 +1,39 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
public class SignalServicePreview {
private final String url;
private final String title;
private final String description;
private final long date;
private final Optional<SignalServiceAttachment> image;
public SignalServicePreview(String url, String title, String description, long date, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.description = description;
this.date = date;
this.image = image;
}
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public long getDate() {
return date;
}
public Optional<SignalServiceAttachment> getImage() {
return image;
}
}

View File

@@ -0,0 +1,48 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
public class SignalServiceStoryMessage {
private final Optional<byte[]> profileKey;
private final Optional<SignalServiceGroupV2> groupContext;
private final Optional<SignalServiceAttachment> fileAttachment;
private final Optional<SignalServiceTextAttachment> textAttachment;
private SignalServiceStoryMessage(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceAttachment fileAttachment,
SignalServiceTextAttachment textAttachment) {
this.profileKey = Optional.fromNullable(profileKey);
this.groupContext = Optional.fromNullable(groupContext);
this.fileAttachment = Optional.fromNullable(fileAttachment);
this.textAttachment = Optional.fromNullable(textAttachment);
}
public static SignalServiceStoryMessage forFileAttachment(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceAttachment fileAttachment) {
return new SignalServiceStoryMessage(profileKey, groupContext, fileAttachment, null);
}
public static SignalServiceStoryMessage forTextAttachment(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceTextAttachment textAttachment) {
return new SignalServiceStoryMessage(profileKey, groupContext, null, textAttachment);
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<SignalServiceGroupV2> getGroupContext() {
return groupContext;
}
public Optional<SignalServiceAttachment> getFileAttachment() {
return fileAttachment;
}
public Optional<SignalServiceTextAttachment> getTextAttachment() {
return textAttachment;
}
}

View File

@@ -0,0 +1,121 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
public class SignalServiceTextAttachment {
private final Optional<String> text;
private final Optional<Style> style;
private final Optional<Integer> textForegroundColor;
private final Optional<Integer> textBackgroundColor;
private final Optional<SignalServicePreview> preview;
private final Optional<Gradient> backgroundGradient;
private final Optional<Integer> backgroundColor;
private SignalServiceTextAttachment(Optional<String> text,
Optional<Style> style,
Optional<Integer> textForegroundColor,
Optional<Integer> textBackgroundColor,
Optional<SignalServicePreview> preview,
Optional<Gradient> backgroundGradient,
Optional<Integer> backgroundColor) {
this.text = text;
this.style = style;
this.textForegroundColor = textForegroundColor;
this.textBackgroundColor = textBackgroundColor;
this.preview = preview;
this.backgroundGradient = backgroundGradient;
this.backgroundColor = backgroundColor;
}
public static SignalServiceTextAttachment forGradientBackground(Optional<String> text,
Optional<Style> style,
Optional<Integer> textForegroundColor,
Optional<Integer> textBackgroundColor,
Optional<SignalServicePreview> preview,
Gradient backgroundGradient) {
return new SignalServiceTextAttachment(text,
style,
textForegroundColor,
textBackgroundColor,
preview,
Optional.of(backgroundGradient),
Optional.absent());
}
public static SignalServiceTextAttachment forSolidBackground(Optional<String> text,
Optional<Style> style,
Optional<Integer> textForegroundColor,
Optional<Integer> textBackgroundColor,
Optional<SignalServicePreview> preview,
int backgroundColor) {
return new SignalServiceTextAttachment(text,
style,
textForegroundColor,
textBackgroundColor,
preview,
Optional.absent(),
Optional.of(backgroundColor));
}
public Optional<String> getText() {
return text;
}
public Optional<Style> getStyle() {
return style;
}
public Optional<Integer> getTextForegroundColor() {
return textForegroundColor;
}
public Optional<Integer> getTextBackgroundColor() {
return textBackgroundColor;
}
public Optional<SignalServicePreview> getPreview() {
return preview;
}
public Optional<Gradient> getBackgroundGradient() {
return backgroundGradient;
}
public Optional<Integer> getBackgroundColor() {
return backgroundColor;
}
public static class Gradient {
private final Optional<Integer> startColor;
private final Optional<Integer> endColor;
private final Optional<Integer> angle;
public Gradient(Optional<Integer> startColor, Optional<Integer> endColor, Optional<Integer> angle) {
this.startColor = startColor;
this.endColor = endColor;
this.angle = angle;
}
public Optional<Integer> getStartColor() {
return startColor;
}
public Optional<Integer> getEndColor() {
return endColor;
}
public Optional<Integer> getAngle() {
return angle;
}
}
public enum Style {
DEFAULT,
REGULAR,
BOLD,
SERIF,
SCRIPT,
CONDENSED,
}
}

View File

@@ -193,6 +193,9 @@ public class SignalServiceProfile {
@JsonProperty
private boolean changeNumber;
@JsonProperty
private boolean stories;
@JsonCreator
public Capabilities() {}
@@ -219,6 +222,10 @@ public class SignalServiceProfile {
public boolean isChangeNumber() {
return changeNumber;
}
public boolean isStories() {
return stories;
}
}
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {

View File

@@ -118,6 +118,10 @@ public final class SignalContactRecord implements SignalRecord {
diff.add("MuteUntil");
}
if (shouldHideStory() != that.shouldHideStory()) {
diff.add("HideStory");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
@@ -184,6 +188,10 @@ public final class SignalContactRecord implements SignalRecord {
return proto.getMutedUntilTimestamp();
}
public boolean shouldHideStory() {
return proto.getHideStory();
}
ContactRecord toProto() {
return proto;
}
@@ -274,6 +282,11 @@ public final class SignalContactRecord implements SignalRecord {
return this;
}
public Builder setHideStory(boolean hideStory) {
builder.setHideStory(hideStory);
return this;
}
private static ContactRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return ContactRecord.parseFrom(serializedUnknowns).toBuilder();

View File

@@ -78,6 +78,10 @@ public final class SignalGroupV2Record implements SignalRecord {
diff.add("NotifyForMentionsWhenMuted");
}
if (shouldHideStory() != that.shouldHideStory()) {
diff.add("HideStory");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
@@ -132,6 +136,9 @@ public final class SignalGroupV2Record implements SignalRecord {
return !proto.getDontNotifyForMentionsIfMuted();
}
public boolean shouldHideStory() {
return proto.getHideStory();
}
GroupV2Record toProto() {
return proto;
@@ -201,6 +208,11 @@ public final class SignalGroupV2Record implements SignalRecord {
return this;
}
public Builder setHideStory(boolean hideStory) {
builder.setHideStory(hideStory);
return this;
}
private static GroupV2Record.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return GroupV2Record.parseFrom(serializedUnknowns).toBuilder();

View File

@@ -45,6 +45,7 @@ message Content {
optional TypingMessage typingMessage = 6;
optional bytes senderKeyDistributionMessage = 7;
optional bytes decryptionErrorMessage = 8;
optional StoryMessage storyMessage = 9;
}
message CallMessage {
@@ -218,14 +219,6 @@ message DataMessage {
optional string organization = 7;
}
message Preview {
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
optional string description = 4;
optional uint64 date = 5;
}
message Sticker {
optional bytes packId = 1;
optional bytes packKey = 2;
@@ -356,6 +349,50 @@ message TypingMessage {
optional bytes groupId = 3;
}
message StoryMessage {
optional bytes profileKey = 1;
optional GroupContextV2 group = 2;
oneof attachment {
AttachmentPointer fileAttachment = 3;
TextAttachment textAttachment = 4;
}
}
message Preview {
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
optional string description = 4;
optional uint64 date = 5;
}
message TextAttachment {
enum Style {
DEFAULT = 0;
REGULAR = 1;
BOLD = 2;
SERIF = 3;
SCRIPT = 4;
CONDENSED = 5;
}
message Gradient {
optional uint32 startColor = 1;
optional uint32 endColor = 2;
optional uint32 angle = 3; // degrees
}
optional string text = 1;
optional Style textStyle = 2;
optional uint32 textForegroundColor = 3; // integer representation of hex color
optional uint32 textBackgroundColor = 4;
optional Preview preview = 5;
oneof background {
Gradient gradient = 6;
uint32 color = 7;
}
}
message Verified {
enum State {
DEFAULT = 0;

View File

@@ -82,6 +82,7 @@ message ContactRecord {
bool archived = 11;
bool markedUnread = 12;
uint64 mutedUntilTimestamp = 13;
bool hideStory = 14;
}
message GroupV1Record {
@@ -101,6 +102,7 @@ message GroupV2Record {
bool markedUnread = 5;
uint64 mutedUntilTimestamp = 6;
bool dontNotifyForMentionsIfMuted = 7;
bool hideStory = 8;
}
message Payments {

View File

@@ -16,7 +16,7 @@ public final class AccountAttributesTest {
"reglock1234",
new byte[10],
false,
new AccountAttributes.Capabilities(true, true, true, true, true, true, true),
new AccountAttributes.Capabilities(true, true, true, true, true, true, true, true),
false,
null));
assertEquals("{\"signalingKey\":\"skey\"," +
@@ -29,19 +29,19 @@ public final class AccountAttributesTest {
"\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," +
"\"unrestrictedUnidentifiedAccess\":false," +
"\"discoverableByPhoneNumber\":false," +
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"gv2-3\":true,\"gv1-migration\":true}," +
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"stories\":true,\"gv2-3\":true,\"gv1-migration\":true}," +
"\"name\":null}", json);
}
@Test
public void gv2_true() {
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
}
@Test
public void gv2_false() {
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
}
}