mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-22 04:56:04 +00:00
Add group story support to core library layer
Extend Manager.sendStory() with an optional GroupId parameter and add SendHelper.sendGroupStoryMessage() for endorsement-aware group story delivery, laying the groundwork for group story support (task 1 of 4). The existing My Story code path is unchanged.
This commit is contained in:
parent
fd98cce8b3
commit
165535585c
@ -221,12 +221,17 @@ public interface Manager extends Closeable {
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
/**
|
||||
* Post a file attachment story to "My Story".
|
||||
* Post a file attachment story to "My Story" or to a group.
|
||||
*
|
||||
* @param attachment path to the file to upload and post as a story
|
||||
* @param allowsReplies whether other users are allowed to reply to this story
|
||||
* @param groupId if present, post the story to this group instead of "My Story"
|
||||
*/
|
||||
SendMessageResults sendStory(String attachment, boolean allowsReplies) throws IOException, AttachmentInvalidException;
|
||||
SendMessageResults sendStory(
|
||||
String attachment,
|
||||
boolean allowsReplies,
|
||||
Optional<GroupId> groupId
|
||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
|
||||
|
||||
SendMessageResults sendRemoteDeleteMessage(
|
||||
long targetSentTimestamp,
|
||||
|
||||
@ -379,6 +379,67 @@ public class SendHelper {
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a story message (file attachment) to a group.
|
||||
*/
|
||||
public List<SendMessageResult> sendGroupStoryMessage(
|
||||
SignalServiceStoryMessage storyMessage,
|
||||
long timestamp,
|
||||
GroupInfoV2 groupInfo,
|
||||
boolean allowsReplies
|
||||
) throws IOException {
|
||||
final var messageSender = dependencies.getMessageSender();
|
||||
|
||||
final var recipientIds = groupInfo.getMembersWithout(account.getSelfRecipientId());
|
||||
final var recipientIdList = List.copyOf(recipientIds);
|
||||
final var addressesMap = recipientIdList.stream()
|
||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||
|
||||
final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo);
|
||||
if (groupSendEndorsementsResult == null) {
|
||||
throw new IOException("Group send endorsements unavailable; try again after group state refreshes");
|
||||
}
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfo.getMasterKey());
|
||||
final var senderCertificate = context.getUnidentifiedAccessHelper().getSenderCertificateFor(null);
|
||||
final var groupSendEndorsements = new GroupSendEndorsements(groupSendEndorsementsResult.first(),
|
||||
recipientIdList.stream()
|
||||
.collect(Collectors.toMap(id -> (ACI) addressesMap.get(id).getServiceId(),
|
||||
groupSendEndorsementsResult.second()::get)),
|
||||
senderCertificate,
|
||||
groupSecretParams);
|
||||
|
||||
final var addresses = recipientIdList.stream().map(addressesMap::get).toList();
|
||||
final var unidentifiedAccesses = recipientIdList.stream().map(unidentifiedAccessesMap::get).toList();
|
||||
final var storyMessageRecipients = recipientIdList.stream()
|
||||
.map(id -> new SignalServiceStoryMessageRecipient(addressesMap.get(id),
|
||||
List.of(groupInfo.getDistributionId().asUuid().toString()),
|
||||
allowsReplies))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
final List<SendMessageResult> results;
|
||||
try {
|
||||
results = messageSender.sendGroupStory(groupInfo.getDistributionId(),
|
||||
Optional.of(groupInfo.getMasterKey().serialize()),
|
||||
addresses,
|
||||
unidentifiedAccesses,
|
||||
groupSendEndorsements,
|
||||
false,
|
||||
storyMessage,
|
||||
timestamp,
|
||||
storyMessageRecipients,
|
||||
null);
|
||||
} catch (UntrustedIdentityException | InvalidKeyException | NoSessionException | InvalidRegistrationIdException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
for (var r : results) {
|
||||
handleSendMessageResult(r);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<SendMessageResult> sendAsGroupMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final GroupInfo g,
|
||||
|
||||
@ -83,6 +83,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;
|
||||
@ -108,9 +109,11 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
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.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
@ -844,8 +847,9 @@ public class ManagerImpl implements Manager {
|
||||
@Override
|
||||
public SendMessageResults sendStory(
|
||||
String attachment,
|
||||
boolean allowsReplies
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
boolean allowsReplies,
|
||||
Optional<GroupId> groupId
|
||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
||||
final var file = new File(attachment);
|
||||
final var mimeType = MimeUtils.getFileMimeType(file);
|
||||
if (mimeType.isEmpty() || (!mimeType.get().startsWith("image/") && !mimeType.get().startsWith("video/"))) {
|
||||
@ -853,6 +857,10 @@ public class ManagerImpl implements Manager {
|
||||
new IOException("Stories only support image and video attachments"));
|
||||
}
|
||||
|
||||
if (groupId.isPresent()) {
|
||||
return sendGroupStory(attachment, allowsReplies, groupId.get());
|
||||
}
|
||||
|
||||
final var recipients = account.getRecipientStore()
|
||||
.getRecipients(true, Optional.of(false), Set.of(), Optional.empty());
|
||||
final var recipientIds = recipients.stream()
|
||||
@ -885,6 +893,57 @@ public class ManagerImpl implements Manager {
|
||||
return new SendMessageResults(timestamp, results);
|
||||
}
|
||||
|
||||
private SendMessageResults sendGroupStory(
|
||||
String attachment,
|
||||
boolean allowsReplies,
|
||||
GroupId groupId
|
||||
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException {
|
||||
final var groupInfo = context.getGroupHelper().getGroup(groupId);
|
||||
if (groupInfo == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
if (!groupInfo.isMember(account.getSelfRecipientId())) {
|
||||
throw new NotAGroupMemberException(groupId, groupInfo.getTitle());
|
||||
}
|
||||
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
||||
throw new IOException("Stories are only supported for V2 groups");
|
||||
}
|
||||
|
||||
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
||||
final var groupContext = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
|
||||
.withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().revision)
|
||||
.build();
|
||||
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
||||
groupContext,
|
||||
uploadedAttachment,
|
||||
allowsReplies,
|
||||
List.of());
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
|
||||
final var sendResults = context.getSendHelper()
|
||||
.sendGroupStoryMessage(storyMessage, timestamp, groupInfoV2, allowsReplies);
|
||||
|
||||
final var storyMessageRecipients = sendResults.stream()
|
||||
.filter(org.whispersystems.signalservice.api.messages.SendMessageResult::isSuccess)
|
||||
.map(r -> new SignalServiceStoryMessageRecipient(r.getAddress(),
|
||||
List.of(groupInfoV2.getDistributionId().asUuid().toString()),
|
||||
allowsReplies))
|
||||
.collect(Collectors.toSet());
|
||||
try {
|
||||
dependencies.getMessageSender().sendStorySyncMessage(storyMessage, timestamp, false, storyMessageRecipients);
|
||||
} catch (UntrustedIdentityException | NoSessionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
for (final var sendResult : sendResults) {
|
||||
final var result = toSendMessageResult(sendResult);
|
||||
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
||||
}
|
||||
|
||||
return new SendMessageResults(timestamp, results);
|
||||
}
|
||||
|
||||
private void applyMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final Message message
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user