Add additional checks for terminated groups during send flows.

This commit is contained in:
Cody Henthorne
2026-03-26 11:58:59 -04:00
parent 467c154ea6
commit 43f19d14d8
12 changed files with 62 additions and 12 deletions

View File

@@ -108,6 +108,11 @@ class AdminDeleteSendJob private constructor(
}
val groupRecord = SignalDatabase.groups.getGroup(conversationRecipient.requireGroupId())
if (groupRecord.isPresent && groupRecord.get().isTerminated) {
Log.w(TAG, "Cannot admin delete in a terminated group.")
return Result.failure()
}
if (groupRecord.isEmpty || !groupRecord.get().isAdmin(Recipient.self())) {
Log.w(TAG, "Cannot delete because you are not an admin.")
return Result.failure()

View File

@@ -7,9 +7,10 @@ import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint;
import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
@@ -121,6 +122,11 @@ public class GroupCallUpdateSendJob extends BaseJob {
throw new AssertionError("We have a recipient, but it's not a V2 Group");
}
if (!SignalDatabase.groups().isActive(conversationRecipient.requireGroupId())) {
Log.w(TAG, "Not sending group call update to terminated or inactive group.");
return;
}
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> completions = deliver(conversationRecipient, destinations);

View File

@@ -101,6 +101,11 @@ class PollVoteJob(
return Result.failure()
}
if (conversationRecipient.isPushV2Group && !SignalDatabase.groups.isActive(conversationRecipient.requireGroupId())) {
Log.w(TAG, "Cannot send poll vote to terminated or inactive group.")
return Result.failure()
}
val poll = SignalDatabase.polls.getPoll(messageId)
if (poll == null) {
Log.w(TAG, "Unable to find corresponding poll")

View File

@@ -169,6 +169,11 @@ public class ReactionSendJob extends BaseJob {
return;
}
if (conversationRecipient.isPushV2Group() && !SignalDatabase.groups().isActive(conversationRecipient.requireGroupId())) {
Log.w(TAG, "Cannot send reactions to terminated or inactive groups.");
return;
}
List<Recipient> resolved = recipients.stream().map(Recipient::resolved).collect(Collectors.toList());
List<RecipientId> unregistered = resolved.stream().filter(Recipient::isUnregistered).map(Recipient::getId).collect(Collectors.toList());
List<Recipient> destinations = resolved.stream().filter(Recipient::isMaybeRegistered).collect(Collectors.toList());

View File

@@ -153,6 +153,11 @@ public class RemoteDeleteSendJob extends BaseJob {
return;
}
if (conversationRecipient.isPushV2Group() && !SignalDatabase.groups().isActive(conversationRecipient.requireGroupId())) {
Log.w(TAG, "Unable to remote delete messages in terminated or inactive groups");
return;
}
List<Recipient> possible = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> eligible = RecipientUtil.getEligibleForSending(Stream.of(recipients).map(Recipient::resolved).filter(Recipient::getHasServiceId).toList());
List<RecipientId> skipped = Stream.of(SetUtil.difference(possible, eligible)).map(Recipient::getId).toList();

View File

@@ -109,6 +109,11 @@ public class TypingSendJob extends BaseJob {
return;
}
if (recipient.isPushV2Group() && !SignalDatabase.groups().isActive(recipient.requireGroupId())) {
Log.w(TAG, "Not sending typing indicators to terminated or inactive groups.");
return;
}
if (!recipient.isRegistered()) {
Log.w(TAG, "Not sending typing indicators to non-Signal recipients.");
return;

View File

@@ -109,6 +109,10 @@ class UnpinMessageJob(
if (conversationRecipient.isPushV2Group) {
val groupRecord = SignalDatabase.groups.getGroup(conversationRecipient.id)
if (groupRecord.isPresent && groupRecord.get().isTerminated) {
Log.w(TAG, "Cannot send unpin messages to terminated group.")
return Result.failure()
}
if (groupRecord.isPresent && groupRecord.get().attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.get().isAdmin(self())) {
Log.w(TAG, "Non-admins cannot send unpin messages to group.")
return Result.failure()

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentSaver
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
@@ -40,14 +41,26 @@ object StoryContextMenu {
private val TAG = Log.tag(StoryContextMenu::class.java)
fun delete(context: Context, records: Set<MessageRecord>): Single<Boolean> {
return DeleteDialog.show(
context = context,
messageRecords = records,
title = context.getString(R.string.MyStories__delete_story),
message = context.getString(R.string.MyStories__this_story_will_be_deleted),
forceRemoteDelete = true
).map { (_, deletedThread) -> deletedThread }
fun delete(context: Context, record: MessageRecord): Single<Boolean> {
val recipient = record.toRecipient
val isGroupTerminated = recipient.isPushV2Group && !SignalDatabase.groups.isActive(recipient.requireGroupId())
return if (isGroupTerminated) {
DeleteDialog.show(
context = context,
messageRecords = setOf(record),
title = context.getString(R.string.MyStories__delete_story),
message = context.getString(R.string.MyStories__delete_story_terminated_group)
).map { (_, deletedThread) -> deletedThread }
} else {
DeleteDialog.show(
context = context,
messageRecords = setOf(record),
title = context.getString(R.string.MyStories__delete_story),
message = context.getString(R.string.MyStories__this_story_will_be_deleted),
forceRemoteDelete = true
).map { (_, deletedThread) -> deletedThread }
}
}
suspend fun save(fragment: Fragment, messageRecord: MessageRecord) {

View File

@@ -327,7 +327,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
}
private fun handleDeleteStory(model: StoriesLandingItem.Model) {
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(model.data.primaryStory.messageRecord)).subscribe()
lifecycleDisposable += StoryContextMenu.delete(requireContext(), model.data.primaryStory.messageRecord).subscribe()
}
private fun handleHideStory(model: StoriesLandingItem.Model) {

View File

@@ -161,7 +161,7 @@ class MyStoriesFragment : DSLSettingsFragment(
}
private fun handleDeleteClick(model: MyStoriesItem.Model) {
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(model.distributionStory.messageRecord)).subscribe()
lifecycleDisposable += StoryContextMenu.delete(requireContext(), model.distributionStory.messageRecord).subscribe()
}
@Suppress("OVERRIDE_DEPRECATION")

View File

@@ -1256,7 +1256,7 @@ class StoryViewerPageFragment :
},
onDelete = {
viewModel.setIsDisplayingDeleteDialog(true)
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(it.conversationMessage.messageRecord)).subscribe { _ ->
lifecycleDisposable += StoryContextMenu.delete(requireContext(), it.conversationMessage.messageRecord).subscribe { _ ->
viewModel.setIsDisplayingDeleteDialog(false)
viewModel.refresh()
}

View File

@@ -6894,6 +6894,8 @@
<string name="MyStories__delete_story">Delete story?</string>
<!-- Message of dialog to confirm deletion of story -->
<string name="MyStories__this_story_will_be_deleted">This story will be deleted for you and everyone who received it.</string>
<!-- Message of dialog to confirm deletion of story in a terminated group -->
<string name="MyStories__delete_story_terminated_group">It will only be deleted for you because the group has ended.</string>
<!-- Toast shown when story media cannot be saved -->
<string name="MyStories__unable_to_save">Unable to save</string>