mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Update spam UX and reporting flows.
This commit is contained in:
committed by
Clark Chen
parent
a4fde60c1c
commit
aa76cefb1c
@@ -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())) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user