mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Implement badge gifting behind feature flag.
This commit is contained in:
committed by
Greyson Parrelli
parent
5d16d1cd23
commit
a4a4665aaa
@@ -968,6 +968,13 @@ public class SignalServiceMessageSender {
|
||||
.setSentTimestamp(storyContext.getSentTimestamp()));
|
||||
}
|
||||
|
||||
if (message.getGiftBadge().isPresent()) {
|
||||
SignalServiceDataMessage.GiftBadge giftBadge = message.getGiftBadge().get();
|
||||
|
||||
builder.setGiftBadge(DataMessage.GiftBadge.newBuilder()
|
||||
.setReceiptCredentialPresentation(ByteString.copyFrom(giftBadge.getReceiptCredentialPresentation().serialize())));
|
||||
}
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build());
|
||||
|
||||
@@ -147,10 +147,13 @@ public class AccountAttributes {
|
||||
@JsonProperty
|
||||
private boolean stories;
|
||||
|
||||
@JsonProperty
|
||||
private boolean giftBadges;
|
||||
|
||||
@JsonCreator
|
||||
public Capabilities() {}
|
||||
|
||||
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories) {
|
||||
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories, boolean giftBadges) {
|
||||
this.uuid = uuid;
|
||||
this.gv2 = gv2;
|
||||
this.storage = storage;
|
||||
@@ -159,6 +162,7 @@ public class AccountAttributes {
|
||||
this.announcementGroup = announcementGroup;
|
||||
this.changeNumber = changeNumber;
|
||||
this.stories = stories;
|
||||
this.giftBadges = giftBadges;
|
||||
}
|
||||
|
||||
public boolean isUuid() {
|
||||
@@ -192,5 +196,9 @@ public class AccountAttributes {
|
||||
public boolean isStories() {
|
||||
return stories;
|
||||
}
|
||||
|
||||
public boolean isGiftBadges() {
|
||||
return giftBadges;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
@@ -614,6 +615,7 @@ public final class SignalServiceContent {
|
||||
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
|
||||
SignalServiceDataMessage.GroupCallUpdate groupCallUpdate = createGroupCallUpdate(content);
|
||||
SignalServiceDataMessage.StoryContext storyContext = createStoryContext(content);
|
||||
SignalServiceDataMessage.GiftBadge giftBadge = createGiftBadge(content);
|
||||
|
||||
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE) {
|
||||
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE,
|
||||
@@ -663,7 +665,8 @@ public final class SignalServiceContent {
|
||||
remoteDelete,
|
||||
groupCallUpdate,
|
||||
payment,
|
||||
storyContext);
|
||||
storyContext,
|
||||
giftBadge);
|
||||
}
|
||||
|
||||
private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata,
|
||||
@@ -1166,6 +1169,23 @@ public final class SignalServiceContent {
|
||||
return new SignalServiceDataMessage.StoryContext(serviceId, content.getStoryContext().getSentTimestamp());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.GiftBadge createGiftBadge(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
|
||||
if (!content.hasGiftBadge()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!content.getGiftBadge().hasReceiptCredentialPresentation()) {
|
||||
throw new InvalidMessageStructureException("GiftBadge does not contain a receipt credential presentation!");
|
||||
}
|
||||
|
||||
try {
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation = new ReceiptCredentialPresentation(content.getGiftBadge().getReceiptCredentialPresentation().toByteArray());
|
||||
return new SignalServiceDataMessage.GiftBadge(receiptCredentialPresentation);
|
||||
} catch (InvalidInputException invalidInputException) {
|
||||
throw new InvalidMessageStructureException(invalidInputException);
|
||||
}
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.PaymentNotification createPaymentNotification(SignalServiceProtos.DataMessage.Payment content)
|
||||
throws InvalidMessageStructureException
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
@@ -42,6 +43,7 @@ public class SignalServiceDataMessage {
|
||||
private final Optional<GroupCallUpdate> groupCallUpdate;
|
||||
private final Optional<Payment> payment;
|
||||
private final Optional<StoryContext> storyContext;
|
||||
private final Optional<GiftBadge> giftBadge;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceDataMessage.
|
||||
@@ -74,7 +76,8 @@ public class SignalServiceDataMessage {
|
||||
RemoteDelete remoteDelete,
|
||||
GroupCallUpdate groupCallUpdate,
|
||||
Payment payment,
|
||||
StoryContext storyContext)
|
||||
StoryContext storyContext,
|
||||
GiftBadge giftBadge)
|
||||
{
|
||||
try {
|
||||
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
|
||||
@@ -97,6 +100,7 @@ public class SignalServiceDataMessage {
|
||||
this.groupCallUpdate = Optional.ofNullable(groupCallUpdate);
|
||||
this.payment = Optional.ofNullable(payment);
|
||||
this.storyContext = Optional.ofNullable(storyContext);
|
||||
this.giftBadge = Optional.ofNullable(giftBadge);
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
this.attachments = Optional.of(attachments);
|
||||
@@ -253,6 +257,10 @@ public class SignalServiceDataMessage {
|
||||
return storyContext;
|
||||
}
|
||||
|
||||
public Optional<GiftBadge> getGiftBadge() {
|
||||
return giftBadge;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getGroupId() {
|
||||
byte[] groupId = null;
|
||||
|
||||
@@ -291,6 +299,7 @@ public class SignalServiceDataMessage {
|
||||
private GroupCallUpdate groupCallUpdate;
|
||||
private Payment payment;
|
||||
private StoryContext storyContext;
|
||||
private GiftBadge giftBadge;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@@ -423,6 +432,11 @@ public class SignalServiceDataMessage {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withGiftBadge(GiftBadge giftBadge) {
|
||||
this.giftBadge = giftBadge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage build() {
|
||||
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
|
||||
@@ -431,7 +445,8 @@ public class SignalServiceDataMessage {
|
||||
mentions, sticker, viewOnce, reaction, remoteDelete,
|
||||
groupCallUpdate,
|
||||
payment,
|
||||
storyContext);
|
||||
storyContext,
|
||||
giftBadge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,4 +672,16 @@ public class SignalServiceDataMessage {
|
||||
return sentTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GiftBadge {
|
||||
private final ReceiptCredentialPresentation receiptCredentialPresentation;
|
||||
|
||||
public GiftBadge(ReceiptCredentialPresentation receiptCredentialPresentation) {
|
||||
this.receiptCredentialPresentation = receiptCredentialPresentation;
|
||||
}
|
||||
|
||||
public ReceiptCredentialPresentation getReceiptCredentialPresentation() {
|
||||
return receiptCredentialPresentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ public class SignalServiceProfile {
|
||||
@JsonProperty
|
||||
private boolean visible;
|
||||
|
||||
@JsonProperty
|
||||
private long duration;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -169,6 +172,13 @@ public class SignalServiceProfile {
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Duration badge is valid for, in seconds.
|
||||
*/
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Capabilities {
|
||||
@@ -190,6 +200,9 @@ public class SignalServiceProfile {
|
||||
@JsonProperty
|
||||
private boolean stories;
|
||||
|
||||
@JsonProperty
|
||||
private boolean giftBadges;
|
||||
|
||||
@JsonCreator
|
||||
public Capabilities() {}
|
||||
|
||||
@@ -216,6 +229,10 @@ public class SignalServiceProfile {
|
||||
public boolean isStories() {
|
||||
return stories;
|
||||
}
|
||||
|
||||
public boolean isGiftBadges() {
|
||||
return giftBadges;
|
||||
}
|
||||
}
|
||||
|
||||
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {
|
||||
|
||||
@@ -20,9 +20,12 @@ import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.rxjava3.annotations.NonNull;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -77,8 +80,8 @@ public class DonationsService {
|
||||
* @param currencyCode The currency code for the amount
|
||||
* @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway.
|
||||
*/
|
||||
public Single<ServiceResponse<SubscriptionClientSecret>> createDonationIntentWithAmount(String amount, String currencyCode, String description) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createBoostPaymentMethod(currencyCode, Long.parseLong(amount), description), 200));
|
||||
public Single<ServiceResponse<SubscriptionClientSecret>> createDonationIntentWithAmount(String amount, String currencyCode, long level) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createBoostPaymentMethod(currencyCode, Long.parseLong(amount), level), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +106,43 @@ public class DonationsService {
|
||||
* @return The badge configuration for signal boost. Expect for right now only a single level numbered 1.
|
||||
*/
|
||||
public Single<ServiceResponse<SignalServiceProfile.Badge>> getBoostBadge(Locale locale) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostLevels(locale).getLevels().get("1").getBadge(), 200));
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostLevels(locale).getLevels().get(SubscriptionLevels.BOOST_LEVEL).getBadge(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A specific gift badge, by level.
|
||||
*/
|
||||
public Single<ServiceResponse<SignalServiceProfile.Badge>> getGiftBadge(Locale locale, long level) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostLevels(locale).getLevels().get(String.valueOf(level)).getBadge(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All gift badges the server currently has available.
|
||||
*/
|
||||
public Single<ServiceResponse<Map<Long, SignalServiceProfile.Badge>>> getGiftBadges(Locale locale) {
|
||||
return createServiceResponse(() -> {
|
||||
Map<String, SubscriptionLevels.Level> levels = pushServiceSocket.getBoostLevels(locale).getLevels();
|
||||
Map<Long, SignalServiceProfile.Badge> badges = new TreeMap<>();
|
||||
|
||||
for (Map.Entry<String, SubscriptionLevels.Level> levelEntry : levels.entrySet()) {
|
||||
if (!Objects.equals(levelEntry.getKey(), SubscriptionLevels.BOOST_LEVEL)) {
|
||||
try {
|
||||
badges.put(Long.parseLong(levelEntry.getKey()), levelEntry.getValue().getBadge());
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Could not parse gift badge for level entry " + levelEntry.getKey(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Pair<>(badges, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amounts for the gift badge.
|
||||
*/
|
||||
public Single<ServiceResponse<Map<String, BigDecimal>>> getGiftAmount() {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getGiftAmount(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,12 +252,12 @@ public class DonationsService {
|
||||
return ServiceResponse.forResult(responseAndCode.first(), responseAndCode.second(), null);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
Log.w(TAG, "Bad response code from server.", e);
|
||||
return ServiceResponse.<T>forApplicationError(e, e.getCode(), e.getMessage());
|
||||
return ServiceResponse.forApplicationError(e, e.getCode(), e.getMessage());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "An unknown error occurred.", e);
|
||||
return ServiceResponse.<T>forUnknownError(e);
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.io());
|
||||
});
|
||||
}
|
||||
|
||||
private interface Producer<T> {
|
||||
|
||||
@@ -13,6 +13,11 @@ import java.util.Map;
|
||||
*/
|
||||
public final class SubscriptionLevels {
|
||||
|
||||
/**
|
||||
* Reserved level for boost badge.
|
||||
*/
|
||||
public static final String BOOST_LEVEL = "1";
|
||||
|
||||
private final Map<String, Level> levels;
|
||||
|
||||
@JsonCreator
|
||||
|
||||
@@ -10,11 +10,11 @@ class DonationIntentPayload {
|
||||
private String currency;
|
||||
|
||||
@JsonProperty
|
||||
private String description;
|
||||
private long level;
|
||||
|
||||
public DonationIntentPayload(long amount, String currency, String description) {
|
||||
public DonationIntentPayload(long amount, String currency, long level) {
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.description = description;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ public class PushServiceSocket {
|
||||
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
|
||||
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
|
||||
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
|
||||
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
|
||||
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
|
||||
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
|
||||
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
|
||||
@@ -896,8 +897,8 @@ public class PushServiceSocket {
|
||||
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
|
||||
}
|
||||
|
||||
public SubscriptionClientSecret createBoostPaymentMethod(String currencyCode, long amount, String description) throws IOException {
|
||||
String payload = JsonUtil.toJson(new DonationIntentPayload(amount, currencyCode, description));
|
||||
public SubscriptionClientSecret createBoostPaymentMethod(String currencyCode, long amount, long level) throws IOException {
|
||||
String payload = JsonUtil.toJson(new DonationIntentPayload(amount, currencyCode, level));
|
||||
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, SubscriptionClientSecret.class);
|
||||
}
|
||||
@@ -908,6 +909,12 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJsonResponse(result, typeRef);
|
||||
}
|
||||
|
||||
public Map<String, BigDecimal> getGiftAmount() throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(GIFT_AMOUNT, "GET", null);
|
||||
TypeReference<HashMap<String, BigDecimal>> typeRef = new TypeReference<HashMap<String, BigDecimal>>() {};
|
||||
return JsonUtil.fromJsonResponse(result, typeRef);
|
||||
}
|
||||
|
||||
public SubscriptionLevels getBoostLevels(Locale locale) throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(BOOST_BADGES, "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale));
|
||||
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
|
||||
|
||||
@@ -287,6 +287,10 @@ message DataMessage {
|
||||
}
|
||||
}
|
||||
|
||||
message GiftBadge {
|
||||
optional bytes receiptCredentialPresentation = 1;
|
||||
}
|
||||
|
||||
enum ProtocolVersion {
|
||||
option allow_alias = true;
|
||||
|
||||
@@ -321,6 +325,7 @@ message DataMessage {
|
||||
optional GroupCallUpdate groupCallUpdate = 19;
|
||||
optional Payment payment = 20;
|
||||
optional StoryContext storyContext = 21;
|
||||
optional GiftBadge giftBadge = 22;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
||||
@@ -16,7 +16,7 @@ public final class AccountAttributesTest {
|
||||
"reglock1234",
|
||||
new byte[10],
|
||||
false,
|
||||
new AccountAttributes.Capabilities(true, true, true, true, true, true, true, true),
|
||||
new AccountAttributes.Capabilities(true, 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,\"stories\":true,\"gv2-3\":true,\"gv1-migration\":true}," +
|
||||
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"stories\":true,\"giftBadges\":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, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false, false, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"giftBadges\":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, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false, false, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"giftBadges\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user