Update spam UX and reporting flows.

This commit is contained in:
Cody Henthorne
2024-02-09 15:25:31 -05:00
committed by Clark Chen
parent a4fde60c1c
commit aa76cefb1c
66 changed files with 1578 additions and 894 deletions

View File

@@ -6,6 +6,7 @@ import android.database.Cursor
import android.text.TextUtils
import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf
import okio.ByteString
import org.intellij.lang.annotations.Language
import org.signal.core.util.SqlUtil
import org.signal.core.util.SqlUtil.appendArg
@@ -32,6 +33,7 @@ import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
@@ -57,6 +59,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI.Companion.parseOrNull
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.io.Closeable
import java.security.SecureRandom
@@ -639,6 +642,27 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
}
fun getGroupInviter(groupId: GroupId): Recipient? {
val groupRecord: Optional<GroupRecord> = getGroup(groupId)
if (groupRecord.isPresent && groupRecord.get().isV2Group) {
val pendingMembers: List<DecryptedPendingMember> = groupRecord.get().requireV2GroupProperties().decryptedGroup.pendingMembers
val invitedByAci: ByteString? = DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requireAci())
.or { DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requirePni()) }
.map { it.addedByAci }
.orElse(null)
if (invitedByAci != null) {
val serviceId: ServiceId? = parseOrNull(invitedByAci)
if (serviceId != null) {
return Recipient.externalPush(serviceId)
}
}
}
return null
}
@CheckReturnValue
fun create(groupId: GroupId.V1, title: String?, members: Collection<RecipientId>, avatar: SignalServiceAttachmentPointer?): Boolean {
if (groupExists(groupId.deriveV2MigrationGroupId())) {

View File

@@ -405,6 +405,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$LATEST_REVISION_ID IS NULL AND
$TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT} = 0 AND
$TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT} = 0 AND
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND
$TYPE NOT IN (
${MessageTypes.PROFILE_CHANGE_TYPE},
${MessageTypes.GV1_MIGRATION_TYPE},
@@ -1728,7 +1730,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$TYPE != ${MessageTypes.CHANGE_NUMBER_TYPE} AND
$TYPE != ${MessageTypes.SMS_EXPORT_TYPE} AND
$TYPE != ${MessageTypes.BOOST_REQUEST_TYPE} AND
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS}
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED}
)
"""
@@ -2388,6 +2392,18 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
sentTimeMillis = timestamp,
expiresIn = expiresIn
)
} else if (MessageTypes.isReportedSpam(outboxType)) {
OutgoingMessage.reportSpamMessage(
threadRecipient = threadRecipient,
sentTimeMillis = timestamp,
expiresIn = expiresIn
)
} else if (MessageTypes.isMessageRequestAccepted(outboxType)) {
OutgoingMessage.messageRequestAcceptMessage(
threadRecipient = threadRecipient,
sentTimeMillis = timestamp,
expiresIn = expiresIn
)
} else {
val giftBadge: GiftBadge? = if (body != null && MessageTypes.isGiftBadge(outboxType)) {
GiftBadge.ADAPTER.decode(Base64.decode(body))
@@ -2552,7 +2568,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val isNotStoryGroupReply = retrieved.parentStoryId == null || !retrieved.parentStoryId.isGroupReply()
if (!MessageTypes.isPaymentsActivated(type) && !MessageTypes.isPaymentsRequestToActivate(type) && !MessageTypes.isExpirationTimerUpdate(type) && !retrieved.storyType.isStory && isNotStoryGroupReply && !silent) {
if (!MessageTypes.isPaymentsActivated(type) &&
!MessageTypes.isPaymentsRequestToActivate(type) &&
!MessageTypes.isReportedSpam(type) &&
!MessageTypes.isMessageRequestAccepted(type) &&
!MessageTypes.isExpirationTimerUpdate(type) &&
!retrieved.storyType.isStory &&
isNotStoryGroupReply &&
!silent
) {
val incrementUnreadMentions = retrieved.mentions.isNotEmpty() && retrieved.mentions.any { it.recipientId == Recipient.self().id }
threads.incrementUnread(threadId, 1, if (incrementUnreadMentions) 1 else 0)
ThreadUpdateJob.enqueue(threadId)
@@ -2782,6 +2806,22 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
hasSpecialType = true
}
if (message.isReportSpam) {
if (hasSpecialType) {
throw MmsException("Cannot insert message with multiple special types.")
}
type = type or MessageTypes.SPECIAL_TYPE_REPORTED_SPAM
hasSpecialType = true
}
if (message.isMessageRequestAccept) {
if (hasSpecialType) {
throw MmsException("Cannot insert message with multiple special types.")
}
type = type or MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED
hasSpecialType = true
}
val earlyDeliveryReceipts: Map<RecipientId, Receipt> = earlyDeliveryReceiptCache.remove(message.sentTimeMillis)
if (earlyDeliveryReceipts.isNotEmpty()) {
@@ -3533,6 +3573,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.run()
}
fun hasReportSpamMessage(threadId: Long): Boolean {
return readableDatabase
.exists(TABLE_NAME)
.where("$THREAD_ID = $threadId AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) = ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM}")
.run()
}
private val outgoingInsecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND NOT ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT})"
private val outgoingSecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})"
@@ -4011,6 +4058,33 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.take(limit)
}
fun getGroupReportSpamMessageServerData(threadId: Long, inviter: RecipientId, timestamp: Long, limit: Int): List<ReportSpamData> {
val data: MutableList<ReportSpamData> = ArrayList()
val incomingGroupUpdateClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE} AND ($TYPE & ${MessageTypes.GROUP_UPDATE_BIT}) != 0"
readableDatabase
.select(FROM_RECIPIENT_ID, SERVER_GUID, DATE_RECEIVED)
.from(TABLE_NAME)
.where("$FROM_RECIPIENT_ID = ? AND $THREAD_ID = ? AND $DATE_RECEIVED <= ? AND $incomingGroupUpdateClause", inviter, threadId, timestamp)
.orderBy("$DATE_RECEIVED DESC")
.limit(limit)
.run()
.forEach { cursor ->
val serverGuid: String? = cursor.requireString(SERVER_GUID)
if (serverGuid != null && serverGuid.isNotEmpty()) {
data += ReportSpamData(
recipientId = RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)),
serverGuid = serverGuid,
dateReceived = cursor.requireLong(DATE_RECEIVED)
)
}
}
return data
}
@Throws(NoSuchMessageException::class)
private fun getMessageExportState(messageId: MessageId): MessageExportState {
return readableDatabase

View File

@@ -113,6 +113,8 @@ public interface MessageTypes {
long SPECIAL_TYPE_GIFT_BADGE = 0x200000000L;
long SPECIAL_TYPE_PAYMENTS_NOTIFICATION = 0x300000000L;
long SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST = 0x400000000L;
long SPECIAL_TYPE_REPORTED_SPAM = 0x500000000L;
long SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED = 0x600000000L;
long SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L;
long IGNORABLE_TYPESMASK_WHEN_COUNTING = END_SESSION_BIT | KEY_EXCHANGE_IDENTITY_UPDATE_BIT | KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
@@ -137,6 +139,14 @@ public interface MessageTypes {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_PAYMENTS_ACTIVATED;
}
static boolean isReportedSpam(long type) {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_REPORTED_SPAM;
}
static boolean isMessageRequestAccepted(long type) {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED;
}
static boolean isDraftMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
}

View File

@@ -239,4 +239,12 @@ public abstract class DisplayRecord {
public boolean isPaymentsActivated() {
return MessageTypes.isPaymentsActivated(type);
}
public boolean isReportedSpam() {
return MessageTypes.isReportedSpam(type);
}
public boolean isMessageRequestAccepted() {
return MessageTypes.isMessageRequestAccepted(type);
}
}

View File

@@ -83,45 +83,6 @@ public class InMemoryMessageRecord extends MessageRecord {
return 0;
}
/**
* Warning message to show during message request state if you do not have groups in common
* with an individual or do not know anyone in the group.
*/
public static final class NoGroupsInCommon extends InMemoryMessageRecord {
private final boolean isGroup;
public NoGroupsInCommon(long threadId, boolean isGroup) {
super(NO_GROUPS_IN_COMMON_ID, "", Recipient.UNKNOWN, threadId, 0);
this.isGroup = isGroup;
}
@Override
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
return UpdateDescription.staticDescription(context.getString(isGroup ? R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully
: R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully),
R.drawable.symbol_info_compact_16);
}
@Override
public boolean isUpdate() {
return true;
}
@Override
public boolean showActionButton() {
return true;
}
public boolean isGroup() {
return isGroup;
}
@Override
public @StringRes int getActionButtonText() {
return R.string.ConversationUpdateItem_learn_more;
}
}
public static final class RemovedContactHidden extends InMemoryMessageRecord {
public RemovedContactHidden(long threadId) {

View File

@@ -271,6 +271,10 @@ public abstract class MessageRecord extends DisplayRecord {
} else if (isPaymentsActivated()) {
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_activated_payments), R.drawable.ic_card_activate_payments)
: fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_can_accept_payments, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments);
} else if (isReportedSpam()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_reported_as_spam), R.drawable.symbol_spam_16);
} else if (isMessageRequestAccepted()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_accepted_the_message_request), R.drawable.symbol_thread_16);
}
return null;
@@ -632,7 +636,7 @@ public abstract class MessageRecord extends DisplayRecord {
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() ||
isPaymentsRequestToActivate() || isPaymentsActivated();
isPaymentsRequestToActivate() || isPaymentsActivated() || isReportedSpam() || isMessageRequestAccepted();
}
public boolean isMediaPending() {