use of org.whispersystems.signalservice.api.push.ServiceId in project Signal-Android by WhisperSystems.
the class ProfileService method getProfile.
public Single<ServiceResponse<ProfileAndCredential>> getProfile(SignalServiceAddress address, Optional<ProfileKey> profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, SignalServiceProfile.RequestType requestType, Locale locale) {
ServiceId serviceId = address.getServiceId();
SecureRandom random = new SecureRandom();
ProfileKeyCredentialRequestContext requestContext = null;
WebSocketProtos.WebSocketRequestMessage.Builder builder = WebSocketProtos.WebSocketRequestMessage.newBuilder().setId(random.nextLong()).setVerb("GET");
if (profileKey.isPresent()) {
ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion(serviceId.uuid());
String version = profileKeyIdentifier.serialize();
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, serviceId.uuid(), profileKey.get());
ProfileKeyCredentialRequest request = requestContext.getRequest();
String credentialRequest = Hex.toStringCondensed(request.serialize());
builder.setPath(String.format("/v1/profile/%s/%s/%s", serviceId, version, credentialRequest));
} else {
builder.setPath(String.format("/v1/profile/%s/%s", serviceId, version));
}
} else {
builder.setPath(String.format("/v1/profile/%s", address.getIdentifier()));
}
builder.addHeaders(AcceptLanguagesUtil.getAcceptLanguageHeader(locale));
WebSocketProtos.WebSocketRequestMessage requestMessage = builder.build();
ResponseMapper<ProfileAndCredential> responseMapper = DefaultResponseMapper.extend(ProfileAndCredential.class).withResponseMapper(new ProfileResponseMapper(requestType, requestContext)).build();
return signalWebSocket.request(requestMessage, unidentifiedAccess).map(responseMapper::map).onErrorResumeNext(t -> restFallback(address, profileKey, unidentifiedAccess, requestType, locale)).onErrorReturn(ServiceResponse::forUnknownError);
}
use of org.whispersystems.signalservice.api.push.ServiceId in project Signal-Android by WhisperSystems.
the class RetrieveProfileJob method onRun.
@Override
public void onRun() throws IOException, RetryLaterException {
if (!SignalStore.account().isRegistered()) {
Log.w(TAG, "Unregistered. Skipping.");
return;
}
Stopwatch stopwatch = new Stopwatch("RetrieveProfile");
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
RecipientUtil.ensureUuidsAreAvailable(context, Stream.of(Recipient.resolvedList(recipientIds)).filter(r -> r.getRegistered() != RecipientDatabase.RegisteredState.NOT_REGISTERED).toList());
List<Recipient> recipients = Recipient.resolvedList(recipientIds);
stopwatch.split("resolve-ensure");
ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), ApplicationDependencies.getSignalServiceMessageReceiver(), ApplicationDependencies.getSignalWebSocket());
List<Observable<Pair<Recipient, ServiceResponse<ProfileAndCredential>>>> requests = Stream.of(recipients).filter(Recipient::hasServiceId).map(r -> ProfileUtil.retrieveProfile(context, r, getRequestType(r), profileService).toObservable()).toList();
stopwatch.split("requests");
OperationState operationState = Observable.mergeDelayError(requests).observeOn(Schedulers.io(), true).scan(new OperationState(), (state, pair) -> {
Recipient recipient = pair.first();
ProfileService.ProfileResponseProcessor processor = new ProfileService.ProfileResponseProcessor(pair.second());
if (processor.hasResult()) {
state.profiles.add(processor.getResult(recipient));
} else if (processor.notFound()) {
Log.w(TAG, "Failed to find a profile for " + recipient.getId());
if (recipient.isRegistered()) {
state.unregistered.add(recipient.getId());
}
} else if (processor.genericIoError()) {
state.retries.add(recipient.getId());
} else {
Log.w(TAG, "Failed to retrieve profile for " + recipient.getId());
}
return state;
}).lastOrError().blockingGet();
stopwatch.split("responses");
Set<RecipientId> success = SetUtil.difference(recipientIds, operationState.retries);
Map<RecipientId, ServiceId> newlyRegistered = Stream.of(operationState.profiles).map(Pair::first).filterNot(Recipient::isRegistered).collect(Collectors.toMap(Recipient::getId, r -> r.getServiceId().orNull()));
// noinspection SimplifyStreamApiCallChains
Util.chunk(operationState.profiles, 150).stream().forEach(list -> {
SignalDatabase.runInTransaction(() -> {
for (Pair<Recipient, ProfileAndCredential> profile : list) {
process(profile.first(), profile.second());
}
});
});
recipientDatabase.markProfilesFetched(success, System.currentTimeMillis());
if (operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) {
Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + operationState.unregistered.size() + " users as unregistered.");
recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, operationState.unregistered);
}
stopwatch.split("process");
for (Pair<Recipient, ProfileAndCredential> profile : operationState.profiles) {
setIdentityKey(profile.first(), profile.second().getProfile().getIdentityKey());
}
stopwatch.split("identityKeys");
long keyCount = Stream.of(operationState.profiles).map(Pair::first).map(Recipient::getProfileKey).withoutNulls().count();
Log.d(TAG, String.format(Locale.US, "Started with %d recipient(s). Found %d profile(s), and had keys for %d of them. Will retry %d.", recipients.size(), operationState.profiles.size(), keyCount, operationState.retries.size()));
stopwatch.stop(TAG);
recipientIds.clear();
recipientIds.addAll(operationState.retries);
if (recipientIds.size() > 0) {
throw new RetryLaterException();
}
}
use of org.whispersystems.signalservice.api.push.ServiceId in project Signal-Android by WhisperSystems.
the class SignalServiceMessageSender method sendGroupMessage.
/**
* Will send a message using sender keys to all of the specified recipients. It is assumed that
* all of the recipients have UUIDs.
*
* This method will handle sending out SenderKeyDistributionMessages as necessary.
*/
private List<SendMessageResult> sendGroupMessage(DistributionId distributionId, List<SignalServiceAddress> recipients, List<UnidentifiedAccess> unidentifiedAccess, long timestamp, Content content, ContentHint contentHint, byte[] groupId, boolean online, SenderKeyGroupEvents sendEvents) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException {
if (recipients.isEmpty()) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Empty recipient list!");
return Collections.emptyList();
}
Preconditions.checkArgument(recipients.size() == unidentifiedAccess.size(), "[" + timestamp + "] Unidentified access mismatch!");
Map<ServiceId, UnidentifiedAccess> accessBySid = new HashMap<>();
Iterator<SignalServiceAddress> addressIterator = recipients.iterator();
Iterator<UnidentifiedAccess> accessIterator = unidentifiedAccess.iterator();
while (addressIterator.hasNext()) {
accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next());
}
for (int i = 0; i < RETRY_COUNT; i++) {
GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients);
Set<SignalProtocolAddress> sharedWith = store.getSenderKeySharedWith(distributionId);
List<SignalServiceAddress> needsSenderKey = targetInfo.destinations.stream().filter(a -> !sharedWith.contains(a)).map(a -> ServiceId.parseOrThrow(a.getName())).distinct().map(SignalServiceAddress::new).collect(Collectors.toList());
if (needsSenderKey.size() > 0) {
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKey.size() + " addresses.");
SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = needsSenderKey.stream().map(r -> {
UnidentifiedAccess targetAccess = accessBySid.get(r.getServiceId());
return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess));
}).collect(Collectors.toList());
List<SendMessageResult> results = sendSenderKeyDistributionMessage(distributionId, needsSenderKey, access, message, groupId);
List<SignalServiceAddress> successes = results.stream().filter(SendMessageResult::isSuccess).map(SendMessageResult::getAddress).collect(Collectors.toList());
Set<String> successSids = successes.stream().map(a -> a.getServiceId().toString()).collect(Collectors.toSet());
Set<SignalProtocolAddress> successAddresses = targetInfo.destinations.stream().filter(a -> successSids.contains(a.getName())).collect(Collectors.toSet());
store.markSenderKeySharedWith(distributionId, successAddresses);
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients.");
int failureCount = results.size() - successes.size();
if (failureCount > 0) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Failed to send sender keys to " + failureCount + " recipients. Sending back failed results now.");
List<SendMessageResult> trueFailures = results.stream().filter(r -> !r.isSuccess()).collect(Collectors.toList());
Set<ServiceId> failedAddresses = trueFailures.stream().map(result -> result.getAddress().getServiceId()).collect(Collectors.toSet());
List<SendMessageResult> fakeNetworkFailures = recipients.stream().filter(r -> !failedAddresses.contains(r.getServiceId())).map(SendMessageResult::networkFailure).collect(Collectors.toList());
List<SendMessageResult> modifiedResults = new LinkedList<>();
modifiedResults.addAll(trueFailures);
modifiedResults.addAll(fakeNetworkFailures);
return modifiedResults;
} else {
targetInfo = buildGroupTargetInfo(recipients);
}
}
sendEvents.onSenderKeyShared();
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, store, sessionLock, null);
SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate();
byte[] ciphertext;
try {
ciphertext = cipher.encryptForGroup(distributionId, targetInfo.destinations, senderCertificate, content.toByteArray(), contentHint, groupId);
} catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity());
}
sendEvents.onMessageEncrypted();
byte[] joinedUnidentifiedAccess = new byte[16];
for (UnidentifiedAccess access : unidentifiedAccess) {
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey());
}
try {
try {
SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online).blockingGet()).getResultOrThrow();
return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
} catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) {
// Non-technical failures shouldn't be retried with socket
throw e;
} catch (WebSocketUnavailableException e) {
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
} catch (IOException e) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
}
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online);
return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
} catch (GroupMismatchedDevicesException e) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")");
for (GroupMismatchedDevices mismatched : e.getMismatchedDevices()) {
SignalServiceAddress address = new SignalServiceAddress(ACI.parseOrThrow(mismatched.getUuid()), Optional.absent());
handleMismatchedDevices(socket, address, mismatched.getDevices());
}
} catch (GroupStaleDevicesException e) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling stale devices. (" + e.getMessage() + ")");
for (GroupStaleDevices stale : e.getStaleDevices()) {
SignalServiceAddress address = new SignalServiceAddress(ACI.parseOrThrow(stale.getUuid()), Optional.absent());
handleStaleDevices(address, stale.getDevices());
}
}
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Attempt failed (i = " + i + ")");
}
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
}
use of org.whispersystems.signalservice.api.push.ServiceId in project Signal-Android by WhisperSystems.
the class ConversationUpdateItem method present.
private void present(@NonNull ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Recipient conversationRecipient, boolean isMessageRequestAccepted) {
Set<MultiselectPart> multiselectParts = conversationMessage.getMultiselectCollection().toSet();
setSelected(!Sets.intersection(multiselectParts, batchSelected).isEmpty());
if (conversationMessage.getMessageRecord().isGroupV1MigrationEvent() && (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isGroupV1MigrationEvent())) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges());
}
});
} else if (conversationMessage.getMessageRecord().isChatSessionRefresh() && (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isChatSessionRefresh())) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onChatSessionRefreshLearnMoreClicked();
}
});
} else if (conversationMessage.getMessageRecord().isIdentityUpdate()) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onSafetyNumberLearnMoreClicked(conversationMessage.getMessageRecord().getIndividualRecipient());
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ServiceId> acis = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(acis)) {
if (acis.contains(Recipient.self().requireServiceId())) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;
} else {
text = R.string.ConversationUpdateItem_join_call;
}
}
if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) {
actionButton.setText(text);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onJoinGroupCallClicked();
}
});
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
} else if (conversationMessage.getMessageRecord().isSelfCreatedGroup()) {
actionButton.setText(R.string.ConversationUpdateItem_invite_friends);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onInviteFriendsToGroupClicked(conversationRecipient.requireGroupId().requireV2());
}
});
} else if ((conversationMessage.getMessageRecord().isMissedAudioCall() || conversationMessage.getMessageRecord().isMissedVideoCall()) && EnableCallNotificationSettingsDialog.shouldShow(getContext())) {
actionButton.setVisibility(VISIBLE);
actionButton.setText(R.string.ConversationUpdateItem_enable_call_notifications);
actionButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onEnableCallNotificationsClicked();
}
});
} else if (conversationMessage.getMessageRecord().isInMemoryMessageRecord() && ((InMemoryMessageRecord) conversationMessage.getMessageRecord()).showActionButton()) {
InMemoryMessageRecord inMemoryMessageRecord = (InMemoryMessageRecord) conversationMessage.getMessageRecord();
actionButton.setVisibility(VISIBLE);
actionButton.setText(inMemoryMessageRecord.getActionButtonText());
actionButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onInMemoryMessageClicked(inMemoryMessageRecord);
}
});
} else if (conversationMessage.getMessageRecord().isGroupV2DescriptionUpdate()) {
actionButton.setVisibility(VISIBLE);
actionButton.setText(R.string.ConversationUpdateItem_view);
actionButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onViewGroupDescriptionChange(conversationRecipient.getGroupId().orNull(), conversationMessage.getMessageRecord().getGroupV2DescriptionUpdate(), isMessageRequestAccepted);
}
});
} else if (conversationMessage.getMessageRecord().isBadDecryptType() && (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isBadDecryptType())) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onBadDecryptLearnMoreClicked(conversationMessage.getMessageRecord().getRecipient().getId());
}
});
} else if (conversationMessage.getMessageRecord().isChangeNumber() && conversationMessage.getMessageRecord().getIndividualRecipient().isSystemContact()) {
actionButton.setText(R.string.ConversationUpdateItem_update_contact);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onChangeNumberUpdateContact(conversationMessage.getMessageRecord().getIndividualRecipient());
}
});
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
if (conversationMessage.getMessageRecord().isBoostRequest()) {
actionButton.setVisibility(GONE);
CardView donateButton = donateButtonStub.get();
TextView buttonText = donateButton.findViewById(R.id.conversation_update_donate_action_button);
boolean isSustainer = SignalStore.donationsValues().isLikelyASustainer();
donateButton.setVisibility(VISIBLE);
donateButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onDonateClicked();
}
});
if (isSustainer) {
buttonText.setText(R.string.ConversationUpdateItem_signal_boost);
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_boost_outline_16, 0, 0, 0);
} else {
buttonText.setText(R.string.ConversationUpdateItem_become_a_sustainer);
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
}
AutoRounder.autoSetCorners(donateButton, donateButton::setRadius);
} else if (donateButtonStub.resolved()) {
donateButtonStub.get().setVisibility(GONE);
}
}
use of org.whispersystems.signalservice.api.push.ServiceId in project Signal-Android by WhisperSystems.
the class ContactRecordProcessor method merge.
@NonNull
SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) {
String givenName;
String familyName;
if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) {
givenName = remote.getGivenName().or("");
familyName = remote.getFamilyName().or("");
} else {
givenName = local.getGivenName().or("");
familyName = local.getFamilyName().or("");
}
byte[] unknownFields = remote.serializeUnknownFields();
ServiceId serviceId = local.getAddress().getServiceId() == ServiceId.UNKNOWN ? remote.getAddress().getServiceId() : local.getAddress().getServiceId();
String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull();
SignalServiceAddress address = new SignalServiceAddress(serviceId, e164);
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
String username = remote.getUsername().or(local.getUsername()).or("");
IdentityState identityState = remote.getIdentityState();
byte[] identityKey = remote.getIdentityKey().or(local.getIdentityKey()).orNull();
boolean blocked = remote.isBlocked();
boolean profileSharing = remote.isProfileSharingEnabled();
boolean archived = remote.isArchived();
boolean forcedUnread = remote.isForcedUnread();
long muteUntil = remote.getMuteUntil();
boolean matchesRemote = doParamsMatch(remote, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil);
boolean matchesLocal = doParamsMatch(local, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil);
if (matchesRemote) {
return remote;
} else if (matchesLocal) {
return local;
} else {
return new SignalContactRecord.Builder(keyGenerator.generate(), address, unknownFields).setGivenName(givenName).setFamilyName(familyName).setProfileKey(profileKey).setUsername(username).setIdentityState(identityState).setIdentityKey(identityKey).setBlocked(blocked).setProfileSharingEnabled(profileSharing).setArchived(archived).setForcedUnread(forcedUnread).setMuteUntil(muteUntil).build();
}
}
Aggregations