From 43f19d14d8cd5047b68d4ad6ae1b4660d9694384 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 26 Mar 2026 11:58:59 -0400 Subject: [PATCH] Add additional checks for terminated groups during send flows. --- .../securesms/jobs/AdminDeleteSendJob.kt | 5 ++++ .../jobs/GroupCallUpdateSendJob.java | 8 ++++- .../securesms/jobs/PollVoteJob.kt | 5 ++++ .../securesms/jobs/ReactionSendJob.java | 5 ++++ .../securesms/jobs/RemoteDeleteSendJob.java | 5 ++++ .../securesms/jobs/TypingSendJob.java | 5 ++++ .../securesms/jobs/UnpinMessageJob.kt | 4 +++ .../stories/dialogs/StoryContextMenu.kt | 29 ++++++++++++++----- .../stories/landing/StoriesLandingFragment.kt | 2 +- .../securesms/stories/my/MyStoriesFragment.kt | 2 +- .../viewer/page/StoryViewerPageFragment.kt | 2 +- app/src/main/res/values/strings.xml | 2 ++ 12 files changed, 62 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt index 8d7e7e2510..d32107f517 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java index fcf5f13048..98bdfd049b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java @@ -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 destinations = Stream.of(recipients).map(Recipient::resolved).toList(); List completions = deliver(conversationRecipient, destinations); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt index 507c7dc6fa..4f66e03e56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java index c9d3d4466d..0c8e4e0949 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -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 resolved = recipients.stream().map(Recipient::resolved).collect(Collectors.toList()); List unregistered = resolved.stream().filter(Recipient::isUnregistered).map(Recipient::getId).collect(Collectors.toList()); List destinations = resolved.stream().filter(Recipient::isMaybeRegistered).collect(Collectors.toList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java index 1ae49324fb..b89894b182 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java @@ -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 possible = Stream.of(recipients).map(Recipient::resolved).toList(); List eligible = RecipientUtil.getEligibleForSending(Stream.of(recipients).map(Recipient::resolved).filter(Recipient::getHasServiceId).toList()); List skipped = Stream.of(SetUtil.difference(possible, eligible)).map(Recipient::getId).toList(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java index 8951e62fcc..171800cf76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UnpinMessageJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/UnpinMessageJob.kt index e0d40aa14f..9f503afdd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UnpinMessageJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/UnpinMessageJob.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt index 45025c49ca..cb63cf5dd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryContextMenu.kt @@ -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): Single { - 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 { + 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index b44103d4da..f51243601a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index ca6b6a0abd..dfc971e0d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 3c4d7ffa1d..90f5f99389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -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() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4f7880704..dce94e0216 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6894,6 +6894,8 @@ Delete story? This story will be deleted for you and everyone who received it. + + It will only be deleted for you because the group has ended. Unable to save