This commit is contained in:
Scott Lewis 2025-03-16 19:37:19 +00:00 committed by GitHub
commit fa50d3188b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 213 additions and 31 deletions

View file

@ -368,4 +368,7 @@ public interface Manager extends Closeable {
void handleMessage(MessageEnvelope envelope, Throwable e);
}
SendMessageResults sendStoryMessage(Message message, RecipientIdentifier.Group recipient)
throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
}

View file

@ -31,6 +31,8 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@ -53,6 +55,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import okio.ByteString;
@ -331,6 +334,66 @@ public class SendHelper {
editTargetTimestamp);
}
public List<SendMessageResult> sendGroupStoryMessage(
final SignalServiceStoryMessage message,
final GroupInfo g
) throws IOException {
final var messageSender = dependencies.getMessageSender();
final var messageSendLogStore = account.getMessageSendLogStore();
final AtomicLong entryId = new AtomicLong(-1);
final boolean urgent = true;
final long timestamp = System.currentTimeMillis();
// remove sender/self
final Set<RecipientId> recipientIds = g.getMembersWithout(account.getSelfRecipientId());
final List<String> distributionListIds = List.of(g.getGroupId().toBase64());
final Set<SignalServiceStoryMessageRecipient> messageRecipients = recipientIds.stream().map(i -> {
SignalServiceAddress ssa = context.getRecipientHelper().resolveSignalServiceAddress(i);
return new SignalServiceStoryMessageRecipient(ssa, distributionListIds, true);
}).collect(Collectors.toSet());
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, groupSendEndorsements, isRecipientUpdate) -> messageSender.sendGroupStory(
g.getDistributionId(),
Optional.of(g.getGroupId().serialize()),
recipients,
unidentifiedAccess,
groupSendEndorsements,
true,
message,
timestamp,
messageRecipients,
sendResult -> {
logger.trace("Partial message send results: {}", sendResult.size());
synchronized (entryId) {
if (entryId.get() == -1) {
final var newId = messageSendLogStore.insertIfPossible(timestamp,
sendResult,
ContentHint.RESENDABLE,
urgent);
entryId.set(newId);
} else {
messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
}
}
synchronized (entryId) {
if (entryId.get() == -1) {
final var newId = messageSendLogStore.insertIfPossible(timestamp,
sendResult,
ContentHint.RESENDABLE,
urgent);
entryId.set(newId);
} else {
messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
}
}
});
final var results = sendStoryMessageInternal(senderKeySender, recipientIds, g.getDistributionId());
for (var r : results) {
handleSendMessageResult(r);
}
return results;
}
private List<SendMessageResult> sendGroupMessage(
final SignalServiceDataMessage message,
final Set<RecipientId> recipientIds,
@ -466,6 +529,44 @@ public class SendHelper {
return g;
}
private List<SendMessageResult> sendStoryMessageInternal(
final SenderKeySenderHandler senderKeySender,
final Set<RecipientId> recipientIds,
final DistributionId distributionId
) throws IOException {
long startTime = System.currentTimeMillis();
Set<RecipientId> senderKeyTargets = distributionId == null
? Set.of()
: getSenderKeyCapableRecipientIds(recipientIds);
final var allResults = new ArrayList<SendMessageResult>();
if (!senderKeyTargets.isEmpty()) {
final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
senderKeyTargets,
distributionId,
false);
if (results == null) {
senderKeyTargets = Set.of();
} else {
results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add);
final var recipientResolver = account.getRecipientResolver();
final var failedTargets = results.stream()
.filter(r -> !r.isSuccess())
.map(r -> recipientResolver.resolveRecipient(r.getAddress()))
.toList();
if (!failedTargets.isEmpty()) {
senderKeyTargets = new HashSet<>(senderKeyTargets);
failedTargets.forEach(senderKeyTargets::remove);
}
}
}
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
logger.debug("Sending took {}", duration.toString());
return allResults;
}
private List<SendMessageResult> sendGroupMessageInternal(
final LegacySenderHandler legacySender,
final SenderKeySenderHandler senderKeySender,
@ -546,12 +647,12 @@ public class SendHelper {
senderKeyTargets.add(recipientId);
}
/*
if (senderKeyTargets.size() < 2) {
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
return Set.of();
}
*/
logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size());
return senderKeyTargets;
}

View file

@ -16,6 +16,34 @@
*/
package org.asamk.signal.manager.internal;
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
@ -78,6 +106,7 @@ import org.asamk.signal.manager.storage.AttachmentStore;
import org.asamk.signal.manager.storage.AvatarStore;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
@ -97,6 +126,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
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.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
@ -109,41 +139,14 @@ import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.push.BodyRange;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okio.Utf8;
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
public class ManagerImpl implements Manager {
private static final Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
@ -750,6 +753,35 @@ public class ManagerImpl implements Manager {
return sendMessage(messageBuilder, recipients, notifySelf);
}
@Override
public SendMessageResults sendStoryMessage(Message message, RecipientIdentifier.Group idGroup
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var selfProfile = context.getProfileHelper().getSelfProfile();
if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
logger.warn(
"No profile name set. When sending a message it's recommended to set a profile name with the updateProfile command. This may become mandatory in the future.");
}
final var profileKey = account.getProfileKey().serialize();
GroupInfoV2 groupInfo = (GroupInfoV2) context.getGroupHelper().getGroup(idGroup.groupId());
List<String> attachments = message.attachments();
List<BodyRange> bodyRanges = message.textStyles().stream().map(t -> t.toBodyRange()).toList();
SignalServiceStoryMessage storyMessage = null;
if (attachments != null && attachments.size() > 0) {
var attachment = context.getAttachmentHelper().uploadAttachment(attachments.get(0));
storyMessage = SignalServiceStoryMessage.forFileAttachment(profileKey, null, attachment, true, bodyRanges);
} else {
//SignalServiceTextAttachment textBuilder = new SignalServiceTextAttachment.
//storyMessage = SignalServiceStoryMessage.forTextAttachment(profileKey, ssgroup, textBuilder.build(), true, bodyRanges);
}
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis();
final var result = context.getSendHelper().sendGroupStoryMessage(storyMessage, groupInfo);
results.put(idGroup, result.stream().map(this::toSendMessageResult).toList());
return new SendMessageResults(timestamp, results);
}
@Override
public SendMessageResults sendEditMessage(
Message message,

View file

@ -754,4 +754,6 @@ public interface Signal extends DBusInterface {
}
}
}
long sendStoryMessage(String messageText, List<String> attachments, byte[] groupId);
}

View file

@ -124,6 +124,13 @@ public class SendCommand implements JsonRpcLocalCommand {
groupIdStrings,
usernameStrings);
boolean isStory = false;
if (recipientIdentifiers.size() > 0
&& recipientIdentifiers.iterator().next() instanceof RecipientIdentifier.Group) {
isStory = Boolean.TRUE.equals(ns.getBoolean("story"));
}
final var isEndSession = Boolean.TRUE.equals(ns.getBoolean("end-session"));
if (isEndSession) {
final var singleRecipients = recipientIdentifiers.stream()
@ -247,7 +254,8 @@ public class SendCommand implements JsonRpcLocalCommand {
textStyles);
var results = editTimestamp != null
? m.sendEditMessage(message, recipientIdentifiers, editTimestamp)
: m.sendMessage(message, recipientIdentifiers, notifySelf);
: isStory ? m.sendStoryMessage(message, (RecipientIdentifier.Group) recipientIdentifiers.iterator().next())
: m.sendMessage(message, recipientIdentifiers, notifySelf);
outputResult(outputWriter, results);
} catch (AttachmentInvalidException | IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()

View file

@ -427,6 +427,16 @@ public class DbusManagerImpl implements Manager {
groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
}
@Override
public SendMessageResults sendStoryMessage(
final Message message, final RecipientIdentifier.Group recipient
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(Set.of(recipient),
numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
() -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
groupId -> signal.sendStoryMessage(message.messageText(), message.attachments(), groupId));
}
@Override
public SendMessageResults sendEditMessage(
final Message message,
@ -1150,4 +1160,5 @@ public class DbusManagerImpl implements Manager {
private <T> T getValue(final Map<String, Variant<?>> stringVariantMap, final String field) {
return (T) stringVariantMap.get(field).getValue();
}
}

View file

@ -464,6 +464,31 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
}
}
@Override
public long sendStoryMessage(final String messageText, final List<String> attachments, final byte[] groupId) {
try {
final var message = new Message(messageText,
attachments,
List.of(),
Optional.empty(),
Optional.empty(),
List.of(),
Optional.empty(),
List.of());
var results = m.sendStoryMessage(message,getGroupRecipientIdentifier(groupId));
checkSendMessageResults(results);
return results.timestamp();
} catch (IOException | InvalidStickerException e) {
throw new Error.Failure(e.getMessage());
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (AttachmentInvalidException e) {
throw new Error.AttachmentInvalid(e.getMessage());
} catch (UnregisteredRecipientException e) {
throw new Error.UntrustedIdentity(e.getSender().getIdentifier() + " is not registered.");
}
}
@Override
public void sendGroupTyping(
final byte[] groupId,