From 55acd0f0483d17663525695fe0cac75f27473293 Mon Sep 17 00:00:00 2001
From: Cody Henthorne <cody@signal.org>
Date: Wed, 20 Apr 2022 09:48:44 -0400
Subject: [PATCH] Auto-leave group if added by blocked user.

---
 .../v2/processing/GroupsV2StateProcessor.java | 13 ++--
 .../securesms/jobs/JobManagerFactories.java   |  2 +
 .../securesms/jobs/LeaveGroupV2Job.kt         | 55 ++++++++++++++
 .../securesms/jobs/LeaveGroupV2WorkerJob.kt   | 76 +++++++++++++++++++
 .../securesms/SpinnerApplicationContext.kt    |  3 +-
 .../database/TimestampTransformer.kt          | 26 +++++++
 6 files changed, 168 insertions(+), 7 deletions(-)
 create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2Job.kt
 create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt
 create mode 100644 app/src/spinner/java/org/thoughtcrime/securesms/database/TimestampTransformer.kt

diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java
index eff5dd8bf5..16fe622177 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
 import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
 import org.thoughtcrime.securesms.jobmanager.Job;
 import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
+import org.thoughtcrime.securesms.jobs.LeaveGroupV2Job;
 import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
 import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
 import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -111,11 +112,6 @@ public class GroupsV2StateProcessor {
   }
 
   public enum GroupState {
-    /**
-     * The message revision was inconsistent with server revision, should ignore
-     */
-    INCONSISTENT,
-
     /**
      * The local group was successfully updated to be consistent with the message revision
      */
@@ -590,7 +586,12 @@ public class GroupsV2StateProcessor {
 
           Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId()));
 
-          if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
+          if (addedBy.isBlocked()) {
+            Log.i(TAG, "Added by a blocked user. Leaving group.");
+            ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId));
+            //noinspection UnnecessaryReturnStatement
+            return;
+          } else if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
             Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact() + ", profileSharing: " + addedBy.isProfileSharing());
             Log.i(TAG, "Added to a group and auto-enabling profile sharing");
             recipientDatabase.setProfileSharing(Recipient.externalGroupExact(context, groupId).getId(), true);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index 90936c3bfd..03ffb22514 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -100,6 +100,8 @@ public final class JobManagerFactories {
       put(GroupCallPeekWorkerJob.KEY,                new GroupCallPeekWorkerJob.Factory());
       put(GroupV2UpdateSelfProfileKeyJob.KEY,        new GroupV2UpdateSelfProfileKeyJob.Factory());
       put(KbsEnclaveMigrationWorkerJob.KEY,          new KbsEnclaveMigrationWorkerJob.Factory());
+      put(LeaveGroupV2Job.KEY,                       new LeaveGroupV2Job.Factory());
+      put(LeaveGroupV2WorkerJob.KEY,                 new LeaveGroupV2WorkerJob.Factory());
       put(LocalBackupJob.KEY,                        new LocalBackupJob.Factory());
       put(LocalBackupJobApi29.KEY,                   new LocalBackupJobApi29.Factory());
       put(MarkerJob.KEY,                             new MarkerJob.Factory());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2Job.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2Job.kt
new file mode 100644
index 0000000000..7679cab9c3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2Job.kt
@@ -0,0 +1,55 @@
+package org.thoughtcrime.securesms.jobs
+
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.jobmanager.Data
+import org.thoughtcrime.securesms.jobmanager.Job
+import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint
+
+/**
+ * During group state processing we sometimes detect situations where we should auto-leave. For example,
+ * being added to a group by someone we've blocked. This job functions similarly to other GV2 related
+ * jobs in that it waits for all decryptions to occur and then enqueues the actual [LeaveGroupV2Job] as
+ * part of the group's message processing queue.
+ */
+class LeaveGroupV2Job(parameters: Parameters, private val groupId: GroupId.V2) : BaseJob(parameters) {
+
+  constructor(groupId: GroupId.V2) : this(
+    parameters = Parameters.Builder()
+      .setQueue("LeaveGroupV2Job")
+      .addConstraint(DecryptionsDrainedConstraint.KEY)
+      .setMaxAttempts(Parameters.UNLIMITED)
+      .build(),
+    groupId = groupId
+  )
+
+  override fun serialize(): Data {
+    return Data.Builder()
+      .putString(KEY_GROUP_ID, groupId.toString())
+      .build()
+  }
+
+  override fun getFactoryKey(): String {
+    return KEY
+  }
+
+  override fun onRun() {
+    ApplicationDependencies.getJobManager().add(LeaveGroupV2WorkerJob(groupId))
+  }
+
+  override fun onShouldRetry(e: Exception): Boolean = false
+
+  override fun onFailure() = Unit
+
+  class Factory : Job.Factory<LeaveGroupV2Job> {
+    override fun create(parameters: Parameters, data: Data): LeaveGroupV2Job {
+      return LeaveGroupV2Job(parameters, GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2())
+    }
+  }
+
+  companion object {
+    const val KEY = "LeaveGroupV2Job"
+
+    private const val KEY_GROUP_ID = "group_id"
+  }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt
new file mode 100644
index 0000000000..1894bef092
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupV2WorkerJob.kt
@@ -0,0 +1,76 @@
+package org.thoughtcrime.securesms.jobs
+
+import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.database.SignalDatabase
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
+import org.thoughtcrime.securesms.groups.GroupChangeBusyException
+import org.thoughtcrime.securesms.groups.GroupChangeFailedException
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.GroupManager
+import org.thoughtcrime.securesms.jobmanager.Data
+import org.thoughtcrime.securesms.jobmanager.Job
+import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
+import org.thoughtcrime.securesms.recipients.Recipient
+import java.io.IOException
+
+/**
+ * Leave a group. See [LeaveGroupV2Job] for more details on how this job should be enqueued.
+ */
+class LeaveGroupV2WorkerJob(parameters: Parameters, private val groupId: GroupId.V2) : BaseJob(parameters) {
+
+  constructor(groupId: GroupId.V2) : this(
+    parameters = Parameters.Builder()
+      .setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(ApplicationDependencies.getApplication(), groupId).id))
+      .addConstraint(NetworkConstraint.KEY)
+      .setMaxAttempts(Parameters.UNLIMITED)
+      .setMaxInstancesForQueue(2)
+      .build(),
+    groupId = groupId
+  )
+
+  override fun serialize(): Data {
+    return Data.Builder()
+      .putString(KEY_GROUP_ID, groupId.toString())
+      .build()
+  }
+
+  override fun getFactoryKey(): String {
+    return KEY
+  }
+
+  override fun onRun() {
+    Log.i(TAG, "Attempting to leave group $groupId")
+
+    val groupRecipient = Recipient.externalGroupExact(ApplicationDependencies.getApplication(), groupId)
+
+    GroupManager.leaveGroup(context, groupId)
+
+    val threadId = SignalDatabase.threads.getThreadIdIfExistsFor(groupRecipient.id)
+    if (threadId != -1L) {
+      SignalDatabase.recipients.setProfileSharing(groupRecipient.id, true)
+      SignalDatabase.threads.setEntireThreadRead(threadId)
+      SignalDatabase.threads.update(threadId, false, false)
+      ApplicationDependencies.getMessageNotifier().updateNotification(context)
+    }
+  }
+
+  override fun onShouldRetry(e: Exception): Boolean {
+    return e is GroupChangeBusyException || e is GroupChangeFailedException || e is IOException
+  }
+
+  override fun onFailure() = Unit
+
+  class Factory : Job.Factory<LeaveGroupV2WorkerJob> {
+    override fun create(parameters: Parameters, data: Data): LeaveGroupV2WorkerJob {
+      return LeaveGroupV2WorkerJob(parameters, GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2())
+    }
+  }
+
+  companion object {
+    const val KEY = "LeaveGroupWorkerJob"
+
+    private val TAG = Log.tag(LeaveGroupV2WorkerJob::class.java)
+
+    private const val KEY_GROUP_ID = "group_id"
+  }
+}
diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt
index 462a314cc9..5ffb597956 100644
--- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt
+++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.MegaphoneDatabase
 import org.thoughtcrime.securesms.database.MessageBitmaskColumnTransformer
 import org.thoughtcrime.securesms.database.QueryMonitor
 import org.thoughtcrime.securesms.database.SignalDatabase
+import org.thoughtcrime.securesms.database.TimestampTransformer
 import org.thoughtcrime.securesms.keyvalue.SignalStore
 import org.thoughtcrime.securesms.recipients.Recipient
 import org.thoughtcrime.securesms.util.AppSignatureUtil
@@ -40,7 +41,7 @@ class SpinnerApplicationContext : ApplicationContext() {
       linkedMapOf(
         "signal" to DatabaseConfig(
           db = SignalDatabase.rawDatabase,
-          columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer)
+          columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer, TimestampTransformer)
         ),
         "jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase),
         "keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase),
diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/TimestampTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/TimestampTransformer.kt
new file mode 100644
index 0000000000..91ea68400f
--- /dev/null
+++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/TimestampTransformer.kt
@@ -0,0 +1,26 @@
+package org.thoughtcrime.securesms.database
+
+import android.database.Cursor
+import org.signal.core.util.requireLong
+import org.signal.spinner.ColumnTransformer
+import org.signal.spinner.DefaultColumnTransformer
+import org.thoughtcrime.securesms.util.toLocalDateTime
+import org.thoughtcrime.securesms.util.toMillis
+import java.time.LocalDateTime
+
+object TimestampTransformer : ColumnTransformer {
+  override fun matches(tableName: String?, columnName: String): Boolean {
+    return columnName.contains("date", true) ||
+      columnName.contains("timestamp", true)
+  }
+
+  override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
+    val timestamp: Long = cursor.requireLong(columnName)
+
+    return if (timestamp > LocalDateTime.of(2000, 1, 1, 0, 0, 0, 0).toMillis()) {
+      "$timestamp<br><br>${timestamp.toLocalDateTime()}"
+    } else {
+      DefaultColumnTransformer.transform(tableName, columnName, cursor)
+    }
+  }
+}
-- 
GitLab