Add support for admin delete.

This commit is contained in:
Michelle Tang
2026-02-20 14:44:34 -05:00
committed by Cody Henthorne
parent 1968438ebb
commit 071fbfd916
45 changed files with 648 additions and 132 deletions

View File

@@ -283,6 +283,10 @@ object ImportSkips {
return log(0, "Missing recipient for chat $chatId")
}
fun missingAdminDeleteRecipient(sentTimestamp: Long, chatId: Long): String {
return log(sentTimestamp, "Missing admin delete recipient for chat $chatId")
}
private fun log(sentTimestamp: Long, message: String): String {
return "[SKIP][$sentTimestamp] $message"
}

View File

@@ -44,7 +44,6 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
${MessageTable.FROM_RECIPIENT_ID},
${MessageTable.TO_RECIPIENT_ID},
${MessageTable.EXPIRE_STARTED},
${MessageTable.REMOTE_DELETED},
${MessageTable.UNIDENTIFIED},
${MessageTable.LINK_PREVIEWS},
${MessageTable.SHARED_CONTACTS},
@@ -68,7 +67,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
${MessageTable.VIEW_ONCE},
${MessageTable.PINNED_UNTIL},
${MessageTable.PINNING_MESSAGE_ID},
${MessageTable.PINNED_AT}
${MessageTable.PINNED_AT},
${MessageTable.DELETED_BY}
)
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1
""".trimMargin()
@@ -136,7 +136,6 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
MessageTable.TO_RECIPIENT_ID,
EXPIRES_IN,
MessageTable.EXPIRE_STARTED,
MessageTable.REMOTE_DELETED,
MessageTable.UNIDENTIFIED,
MessageTable.LINK_PREVIEWS,
MessageTable.SHARED_CONTACTS,
@@ -161,7 +160,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
PARENT_STORY_ID,
MessageTable.PINNED_UNTIL,
MessageTable.PINNING_MESSAGE_ID,
MessageTable.PINNED_AT
MessageTable.PINNED_AT,
MessageTable.DELETED_BY
)
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")

View File

@@ -19,7 +19,6 @@ import org.signal.core.util.UuidUtil
import org.signal.core.util.bytes
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.emptyIfNull
import org.signal.core.util.isEmpty
import org.signal.core.util.isNotEmpty
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.kibiBytes
@@ -42,6 +41,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupMode
import org.thoughtcrime.securesms.backup.v2.ExportOddities
import org.thoughtcrime.securesms.backup.v2.ExportSkips
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.proto.AdminDeletedMessage
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
@@ -193,11 +193,16 @@ class ChatItemArchiveExporter(
}
when {
record.remoteDeleted -> {
record.deletedBy == record.fromRecipientId -> {
builder.remoteDeletedMessage = RemoteDeletedMessage()
transformTimer.emit("remote-delete")
}
record.deletedBy != null -> {
builder.adminDeletedMessage = AdminDeletedMessage(adminId = record.deletedBy)
transformTimer.emit("admin-delete")
}
MessageTypes.isJoinedType(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.JOINED_SIGNAL)
transformTimer.emit("simple-update")
@@ -564,7 +569,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien
}
val direction = when {
record.type.isDirectionlessType() && !record.remoteDeleted -> {
record.type.isDirectionlessType() && record.deletedBy == null -> {
Direction.DIRECTIONLESS
}
MessageTypes.isOutgoingMessageType(record.type) || record.fromRecipientId == selfRecipientId.toLong() -> {
@@ -1662,7 +1667,8 @@ private fun ChatItem.validateChatItem(exportState: ExportState, selfRecipientId:
this.giftBadge == null &&
this.viewOnceMessage == null &&
this.directStoryReplyMessage == null &&
this.poll == null
this.poll == null &&
this.adminDeletedMessage == null
) {
Log.w(TAG, ExportSkips.emptyChatItem(this.dateSent))
return null
@@ -1805,7 +1811,6 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set<Long>, backupStartTime: Lo
toRecipientId = this.requireLong(MessageTable.TO_RECIPIENT_ID),
expiresIn = expiresIn,
expireStarted = expireStarted,
remoteDeleted = this.requireBoolean(MessageTable.REMOTE_DELETED),
sealedSender = this.requireBoolean(MessageTable.UNIDENTIFIED),
linkPreview = this.requireString(MessageTable.LINK_PREVIEWS),
sharedContacts = this.requireString(MessageTable.SHARED_CONTACTS),
@@ -1830,6 +1835,7 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set<Long>, backupStartTime: Lo
parentStoryId = this.requireLong(MessageTable.PARENT_STORY_ID),
pinnedAt = this.requireLong(MessageTable.PINNED_AT),
pinnedUntil = this.requireLong(MessageTable.PINNED_UNTIL),
deletedBy = this.requireLongOrNull(MessageTable.DELETED_BY),
messageExtrasSize = messageExtras?.size ?: 0
)
}
@@ -1847,7 +1853,6 @@ private class BackupMessageRecord(
val toRecipientId: Long,
val expiresIn: Long,
val expireStarted: Long,
val remoteDeleted: Boolean,
val sealedSender: Boolean,
val linkPreview: String?,
val sharedContacts: String?,
@@ -1872,6 +1877,7 @@ private class BackupMessageRecord(
val viewOnce: Boolean,
val pinnedAt: Long,
val pinnedUntil: Long,
val deletedBy: Long?,
private val messageExtrasSize: Int
) {
val estimatedSizeInBytes: Int = (body?.length ?: 0) +

View File

@@ -121,7 +121,6 @@ class ChatItemArchiveImporter(
MessageTable.EXPIRES_IN,
MessageTable.EXPIRE_STARTED,
MessageTable.UNIDENTIFIED,
MessageTable.REMOTE_DELETED,
MessageTable.NETWORK_FAILURES,
MessageTable.QUOTE_ID,
MessageTable.QUOTE_AUTHOR,
@@ -141,7 +140,8 @@ class ChatItemArchiveImporter(
MessageTable.NOTIFIED,
MessageTable.PINNED_UNTIL,
MessageTable.PINNING_MESSAGE_ID,
MessageTable.PINNED_AT
MessageTable.PINNED_AT,
MessageTable.DELETED_BY
)
private val REACTION_COLUMNS = arrayOf(
@@ -193,6 +193,12 @@ class ChatItemArchiveImporter(
Log.w(TAG, ImportSkips.chatIdRemoteRecipientNotFound(chatItem.dateSent, chatItem.chatId))
return
}
if (chatItem.adminDeletedMessage != null && importState.remoteToLocalRecipientId[chatItem.adminDeletedMessage.adminId] == null) {
Log.w(TAG, ImportSkips.missingAdminDeleteRecipient(chatItem.dateSent, chatItem.chatId))
return
}
val messageInsert = chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
if (chatItem.revisions.isNotEmpty()) {
// Flush to avoid having revisions cross batch boundaries, which will cause a foreign key failure
@@ -672,7 +678,6 @@ class ChatItemArchiveImporter(
contentValues.put(MessageTable.QUOTE_MISSING, 0)
contentValues.put(MessageTable.QUOTE_TYPE, 0)
contentValues.put(MessageTable.VIEW_ONCE, 0)
contentValues.put(MessageTable.REMOTE_DELETED, 0)
contentValues.put(MessageTable.PARENT_STORY_ID, 0)
if (this.pinDetails != null) {
@@ -683,12 +688,13 @@ class ChatItemArchiveImporter(
when {
this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage)
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.REMOTE_DELETED, 1)
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, fromRecipientId.toLong())
this.updateMessage != null -> contentValues.addUpdateMessage(this.updateMessage, fromRecipientId, toRecipientId)
this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge)
this.viewOnceMessage != null -> contentValues.addViewOnce(this.viewOnceMessage)
this.directStoryReplyMessage != null -> contentValues.addDirectStoryReply(this.directStoryReplyMessage, toRecipientId)
this.adminDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, importState.remoteToLocalRecipientId[this.adminDeletedMessage.adminId]!!.toLong())
}
return contentValues

View File

@@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.fonts.SignalSymbols;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
@@ -1093,16 +1094,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyText.setOverflowText(null);
bodyText.setMaxLength(-1);
if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted);
SpannableString italics = new SpannableString(deletedMessage);
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.signal_text_primary)),
0,
deletedMessage.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
bodyText.setText(italics);
if (conversationMessage.getDeletedByRecipient() != null) {
bodyText.setText(getDeletedMessageText(conversationMessage));
bodyText.setVisibility(View.VISIBLE);
bodyText.setOverflowText(null);
} else if (isCaptionlessMms(messageRecord) || isStoryReaction(messageRecord) || isGiftMessage(messageRecord) || messageRecord.isPaymentNotification() || messageRecord.isPaymentTombstone()) {
@@ -1156,6 +1149,44 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private SpannableStringBuilder getDeletedMessageText(@NonNull ConversationMessage message) {
boolean isAdminDelete = !message.getDeletedByRecipient().equals(message.getMessageRecord().getFromRecipient());
CharSequence body;
if (!isAdminDelete && messageRecord.isOutgoing()) {
body = formatDeletedText(context.getString(R.string.ConversationItem_you_deleted_this_message));
} else if (!isAdminDelete) {
body = formatDeletedText(context.getString(R.string.ConversationItem_this_message_was_deleted));
} else {
SpannableString prefix = formatDeletedText(context.getString(R.string.ConversationItem_admin));
SpannableString suffix = formatDeletedText(context.getString(R.string.ConversationItem_deleted_this_message));
int nameColor = colorizer.getIncomingGroupSenderColor(getContext(), message.getDeletedByRecipient());
SpannableString name = new SpannableString(message.getDeletedByRecipient().getDisplayName(context));
name.setSpan(new ForegroundColorSpan(nameColor), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
name.setSpan(new RecipientClickableSpan(conversationMessage.getDeletedByRecipient().getId()), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
name.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
body = new SpannableStringBuilder()
.append(prefix)
.append(" ")
.append(name)
.append(" ")
.append(suffix);
}
return new SpannableStringBuilder()
.append(SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.X_CIRCLE, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant))
.append(" ")
.append(body);
}
private SpannableString formatDeletedText(String text) {
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, org.signal.core.ui.R.color.signal_colorOnSurfaceVariant)), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableString;
}
private void setMediaAttributes(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousRecord,
@NonNull Optional<MessageRecord> nextRecord,
@@ -1668,7 +1699,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
for (Annotation annotation : mentionAnnotations) {
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
messageBody.setSpan(new RecipientClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
@@ -2895,18 +2926,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class MentionClickableSpan extends ClickableSpan {
private final RecipientId mentionedRecipientId;
private class RecipientClickableSpan extends ClickableSpan {
private final RecipientId recipientId;
MentionClickableSpan(RecipientId mentionedRecipientId) {
this.mentionedRecipientId = mentionedRecipientId;
RecipientClickableSpan(RecipientId recipientId) {
this.recipientId = recipientId;
}
@Override
public void onClick(@NonNull View widget) {
if (eventListener != null && batchSelected.isEmpty()) {
VibrateUtil.vibrateTick(context);
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId());
}
}

View File

@@ -51,6 +51,7 @@ public class ConversationMessage {
@NonNull private final ComputedProperties computedProperties;
@Nullable private final MemberLabel memberLabel;
@Nullable private final MemberLabel quoteMemberLabel;
@Nullable private final Recipient deletedByRecipient;
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@@ -61,7 +62,8 @@ public class ConversationMessage {
@Nullable MessageRecord originalMessage,
@NonNull ComputedProperties computedProperties,
@Nullable MemberLabel memberLabel,
@Nullable MemberLabel quoteMemberLabel)
@Nullable MemberLabel quoteMemberLabel,
@Nullable Recipient deletedByRecipient)
{
this.messageRecord = messageRecord;
this.hasBeenQuoted = hasBeenQuoted;
@@ -72,6 +74,7 @@ public class ConversationMessage {
this.computedProperties = computedProperties;
this.memberLabel = memberLabel;
this.quoteMemberLabel = quoteMemberLabel;
this.deletedByRecipient = deletedByRecipient;
if (body != null) {
this.body = SpannableString.valueOf(body);
@@ -116,6 +119,10 @@ public class ConversationMessage {
return quoteMemberLabel;
}
public @Nullable Recipient getDeletedByRecipient() {
return deletedByRecipient;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -252,6 +259,7 @@ public class ConversationMessage {
FormattedDate formattedDate = getFormattedDate(context, messageRecord);
MemberLabel memberLabel = getMemberLabel(messageRecord, threadRecipient);
MemberLabel quoteMemberLabel = getQuoteMemberLabel(messageRecord, threadRecipient);
Recipient deletedBy = messageRecord.getDeletedBy() != null ? Recipient.resolved(messageRecord.getDeletedBy()) : null;
return new ConversationMessage(messageRecord,
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
@@ -262,7 +270,8 @@ public class ConversationMessage {
originalMessage,
new ComputedProperties(formattedDate),
memberLabel,
quoteMemberLabel);
quoteMemberLabel,
deletedBy);
}
/**

View File

@@ -2828,7 +2828,10 @@ class ConversationFragment :
disposables += DeleteDialog.show(
context = requireContext(),
messageRecords = records
messageRecords = records,
title = requireContext().resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_title, records.size, records.size),
message = requireContext().resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_body, records.size, records.size),
isAdmin = conversationGroupViewModel.isAdmin()
).observeOn(AndroidSchedulers.mainThread())
.subscribe { (deleted: Boolean, _: Boolean) ->
if (!deleted) return@subscribe

View File

@@ -62,6 +62,10 @@ class ConversationGroupViewModel(
disposables.clear()
}
fun isAdmin(): Boolean {
return _memberLevel.value?.groupTableMemberLevel == GroupTable.MemberLevel.ADMINISTRATOR
}
fun isNonAdminInAnnouncementGroup(): Boolean {
val memberLevel = _memberLevel.value ?: return false
return memberLevel.groupTableMemberLevel != GroupTable.MemberLevel.ADMINISTRATOR && memberLevel.isAnnouncementGroup

View File

@@ -534,7 +534,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
} else {
alertView.setNone();
if (thread.getExtra() != null && thread.getExtra().isRemoteDelete()) {
if (thread.getExtra() != null && thread.getExtra().getDeletedBy() != null) {
if (thread.isPending()) {
deliveryStatusIndicator.setPending();
} else {
@@ -697,8 +697,14 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
ThreadTable.Extra extra = thread.getExtra();
if (extra != null && extra.isViewOnce()) {
return emphasisAdded(context, getViewOnceDescription(context, thread.getContentType()), defaultTint);
} else if (extra != null && extra.isRemoteDelete()) {
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
} else if (extra != null && extra.getDeletedBy() != null) {
RecipientId individualRecipientId = thread.getIndividualRecipientId();
RecipientId deletedBy = thread.getDeletedByRecipientId();
if (individualRecipientId.equals(deletedBy)) {
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
} else {
return emphasisAdded(recipientToStringAsync(deletedBy, r -> new SpannableString(context.getString(R.string.ThreadRecord_admin_deleted_this_message, r.getDisplayName(context)))));
}
} else if (extra != null && extra.isPoll()) {
return emphasisAdded(context, thread.getBody(), Glyph.POLL, defaultTint);
} else {

View File

@@ -188,7 +188,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val UNIDENTIFIED = "unidentified"
const val REACTIONS_UNREAD = "reactions_unread"
const val REACTIONS_LAST_SEEN = "reactions_last_seen"
const val REMOTE_DELETED = "remote_deleted"
const val SERVER_GUID = "server_guid"
const val RECEIPT_TIMESTAMP = "receipt_timestamp"
const val EXPORT_STATE = "export_state"
@@ -223,6 +222,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val PINNED_UNTIL = "pinned_until"
const val PINNING_MESSAGE_ID = "pinning_message_id"
const val PINNED_AT = "pinned_at"
const val DELETED_BY = "deleted_by"
const val QUOTE_NOT_PRESENT_ID = 0L
const val QUOTE_TARGET_MISSING_ID = -1L
@@ -273,7 +273,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$VIEW_ONCE INTEGER DEFAULT 0,
$REACTIONS_UNREAD INTEGER DEFAULT 0,
$REACTIONS_LAST_SEEN INTEGER DEFAULT -1,
$REMOTE_DELETED INTEGER DEFAULT 0,
$MENTIONS_SELF INTEGER DEFAULT 0,
$NOTIFIED_TIMESTAMP INTEGER DEFAULT 0,
$SERVER_GUID TEXT DEFAULT NULL,
@@ -292,7 +291,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$VOTES_LAST_SEEN INTEGER DEFAULT 0,
$PINNED_UNTIL INTEGER DEFAULT 0,
$PINNING_MESSAGE_ID INTEGER DEFAULT 0,
$PINNED_AT INTEGER DEFAULT 0
$PINNED_AT INTEGER DEFAULT 0,
$DELETED_BY INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE
)
"""
@@ -325,7 +325,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_UNREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $ORIGINAL_MESSAGE_ID IS NULL AND $READ = 0",
"CREATE INDEX IF NOT EXISTS message_votes_unread_index ON $TABLE_NAME ($VOTES_UNREAD)",
"CREATE INDEX IF NOT EXISTS message_pinned_until_index ON $TABLE_NAME ($PINNED_UNTIL)",
"CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)"
"CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)",
"CREATE INDEX IF NOT EXISTS message_deleted_by_index ON $TABLE_NAME ($DELETED_BY)"
)
private val MMS_PROJECTION_BASE = arrayOf(
@@ -366,7 +367,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
VIEW_ONCE,
REACTIONS_UNREAD,
REACTIONS_LAST_SEEN,
REMOTE_DELETED,
MENTIONS_SELF,
NOTIFIED_TIMESTAMP,
VIEWED_COLUMN,
@@ -381,7 +381,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
MESSAGE_EXTRAS,
VOTES_UNREAD,
VOTES_LAST_SEEN,
PINNED_UNTIL
PINNED_UNTIL,
DELETED_BY
)
private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE + "NULL AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}"
@@ -427,7 +428,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
) AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}
""".toSingleLine()
private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $REMOTE_DELETED = 0"
private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $DELETED_BY IS NULL"
private const val RAW_ID_WHERE = "$TABLE_NAME.$ID = ?"
private val SNIPPET_QUERY =
@@ -1600,7 +1601,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
WHERE
$STORY_TYPE > 0 AND
$REMOTE_DELETED = 0
$DELETED_BY IS NULL
${if (isOutgoingOnly) " AND is_outgoing != 0" else ""}
ORDER BY
is_unread DESC,
@@ -2218,12 +2219,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
AppDependencies.databaseObserver.notifyConversationListListeners()
}
fun markAsRemoteDelete(targetMessage: MessageRecord) {
fun markAsRemoteDelete(targetMessage: MessageRecord, deletedBy: RecipientId) {
writableDatabase.withinTransaction { db ->
val hasRevision = (targetMessage as? MmsMessageRecord)?.latestRevisionId?.id != null
if (hasRevision || targetMessage.isEditMessage) {
val latestRevisionId = (targetMessage as? MmsMessageRecord)?.latestRevisionId?.id ?: targetMessage.id
markAsRemoteDeleteInternal(latestRevisionId)
markAsRemoteDeleteInternal(latestRevisionId, deletedBy)
getPreviousEditIds(latestRevisionId).map { id ->
db.update(TABLE_NAME)
.values(
@@ -2235,22 +2236,22 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
deleteMessage(id)
}
} else {
markAsRemoteDeleteInternal(targetMessage.id)
markAsRemoteDeleteInternal(targetMessage.id, deletedBy)
}
}
}
fun markAsRemoteDelete(messageId: Long) {
fun markAsDeleteBySelf(messageId: Long) {
val targetMessage: MessageRecord = getMessageRecord(messageId)
markAsRemoteDelete(targetMessage)
markAsRemoteDelete(targetMessage, Recipient.self().id)
}
private fun markAsRemoteDeleteInternal(messageId: Long) {
private fun markAsRemoteDeleteInternal(messageId: Long, deletedBy: RecipientId) {
var deletedAttachments = false
writableDatabase.withinTransaction { db ->
db.update(TABLE_NAME)
.values(
REMOTE_DELETED to 1,
DELETED_BY to deletedBy.toLong(),
BODY to null,
QUOTE_BODY to null,
QUOTE_AUTHOR to null,
@@ -4012,7 +4013,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
fun deleteRemotelyDeletedStory(messageId: Long) {
if (readableDatabase.exists(TABLE_NAME).where("$ID = ? AND $REMOTE_DELETED = ?", messageId, 1).run()) {
if (readableDatabase.exists(TABLE_NAME).where("$ID = ? AND $DELETED_BY > 0", messageId).run()) {
deleteMessage(messageId)
} else {
Log.i(TAG, "Unable to delete remotely deleted story: $messageId")
@@ -4595,7 +4596,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val targetMessageDateReceived: Long = readableDatabase
.select(DATE_RECEIVED, LATEST_REVISION_ID)
.from(TABLE_NAME)
.where("$DATE_SENT = $quoteId AND $FROM_RECIPIENT_ID = ? AND $REMOTE_DELETED = 0 AND $SCHEDULED_DATE = -1", authorId)
.where("$DATE_SENT = $quoteId AND $FROM_RECIPIENT_ID = ? AND $DELETED_BY IS NULL AND $SCHEDULED_DATE = -1", authorId)
.run()
.readToSingleObject { cursor ->
val latestRevisionId = cursor.requireLongOrNull(LATEST_REVISION_ID)
@@ -4626,7 +4627,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
fun getMessagePositionInConversation(threadId: Long, receivedTimestamp: Long, authorId: RecipientId): Int {
val validMessageExists: Boolean = readableDatabase
.exists(TABLE_NAME)
.where("$DATE_RECEIVED = $receivedTimestamp AND $FROM_RECIPIENT_ID = ? AND $REMOTE_DELETED = 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL", authorId)
.where("$DATE_RECEIVED = $receivedTimestamp AND $FROM_RECIPIENT_ID = ? AND $DELETED_BY IS NULL AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL", authorId)
.run()
if (!validMessageExists) {
@@ -5455,7 +5456,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.where("$QUOTE_AUTHOR = ?", fromId)
.run()
Log.d(TAG, "Remapped $fromId to $toId. fromRecipient: $fromCount, toRecipient: $toCount, quoteAuthor: $quoteAuthorCount")
val deletedByCount = writableDatabase
.update(TABLE_NAME)
.values(DELETED_BY to toId.serialize())
.where("$DELETED_BY = ?", fromId)
.run()
Log.d(TAG, "Remapped $fromId to $toId. fromRecipient: $fromCount, toRecipient: $toCount, quoteAuthor: $quoteAuthorCount, deletedBy: $deletedByCount")
}
override fun remapThread(fromId: Long, toId: Long) {
@@ -6255,7 +6262,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val expireTimerVersion = cursor.requireInt(EXPIRE_TIMER_VERSION)
val unidentified = cursor.requireBoolean(UNIDENTIFIED)
val isViewOnce = cursor.requireBoolean(VIEW_ONCE)
val remoteDelete = cursor.requireBoolean(REMOTE_DELETED)
val mentionsSelf = cursor.requireBoolean(MENTIONS_SELF)
val notifiedTimestamp = cursor.requireLong(NOTIFIED_TIMESTAMP)
var isViewed = cursor.requireBoolean(VIEWED_COLUMN)
@@ -6269,6 +6275,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val editCount = cursor.requireInt(REVISION_NUMBER)
val isRead = cursor.requireBoolean(READ)
val pinnedUntil = cursor.requireLong(PINNED_UNTIL)
val deletedBy = cursor.requireLongOrNull(DELETED_BY)?.let { RecipientId.from(it) }
val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS)
val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) }
@@ -6346,7 +6353,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
previews,
unidentified,
emptyList(),
remoteDelete,
mentionsSelf,
notifiedTimestamp,
isViewed,
@@ -6364,6 +6370,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
editCount,
isRead,
pinnedUntil,
deletedBy,
messageExtras
)
}

View File

@@ -147,7 +147,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
AND $MESSAGE_ID IN (
SELECT ${MessageTable.ID}
FROM ${MessageTable.TABLE_NAME}
WHERE ${MessageTable.REMOTE_DELETED} = 0
WHERE ${MessageTable.DELETED_BY} IS NULL
)
)
"""
@@ -232,7 +232,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
.where(
"""
$SENT_TIMESTAMP = ? AND
(SELECT ${MessageTable.REMOTE_DELETED} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.ID} = $MESSAGE_ID) = 0
(SELECT ${MessageTable.DELETED_BY} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.ID} = $MESSAGE_ID) IS NULL
""",
sentTimestamp
)
@@ -330,7 +330,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
val messagesWithoutAnyReceivers = localRows.map { it.messageId }.distinct() - remoteRows.map { it.messageId }.distinct()
messagesWithoutAnyReceivers.forEach {
SignalDatabase.messages.markAsRemoteDelete(it)
SignalDatabase.messages.markAsDeleteBySelf(it)
SignalDatabase.messages.deleteRemotelyDeletedStory(it)
}
}
@@ -344,7 +344,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
$TABLE_NAME.$RECIPIENT_ID,
$ALLOWS_REPLIES,
$DISTRIBUTION_ID,
${MessageTable.REMOTE_DELETED}
${MessageTable.DELETED_BY}
FROM $TABLE_NAME
INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID
WHERE $TABLE_NAME.$SENT_TIMESTAMP = ?
@@ -353,7 +353,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas
).use { cursor ->
val results: MutableMap<RecipientId, SentStorySyncManifest.Entry> = mutableMapOf()
while (cursor.moveToNext()) {
val isRemoteDeleted = CursorUtil.requireBoolean(cursor, MessageTable.REMOTE_DELETED)
val isRemoteDeleted = !CursorUtil.isNull(cursor, MessageTable.DELETED_BY)
val recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))
val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DISTRIBUTION_ID))
val distributionIdList: List<DistributionId> = if (isRemoteDeleted) emptyList() else listOf(distributionId)

View File

@@ -2094,7 +2094,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
val extras: Extra? = if (record.isScheduled()) {
Extra.forScheduledMessage(authorId)
} else if (record.isRemoteDelete) {
Extra.forRemoteDelete(authorId)
Extra.forRemoteDelete(authorId, record.deletedBy!!)
} else if (record.isViewOnce) {
Extra.forViewOnce(authorId)
} else if (record.isMms && (record as MmsMessageRecord).slideDeck.stickerSlide != null) {
@@ -2280,7 +2280,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
isSticker = jsonObject.getBoolean("isSticker"),
stickerEmoji = jsonObject.getString("stickerEmoji"),
isAlbum = jsonObject.getBoolean("isAlbum"),
isRemoteDelete = jsonObject.getBoolean("isRemoteDelete"),
deletedBy = jsonObject.getString("deletedBy"),
isMessageRequestAccepted = jsonObject.getBoolean("isMessageRequestAccepted"),
isGv2Invite = jsonObject.getBoolean("isGv2Invite"),
groupAddedBy = jsonObject.getString("groupAddedBy"),
@@ -2353,8 +2353,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
@param:JsonProperty("isAlbum")
val isAlbum: Boolean = false,
@field:JsonProperty
@param:JsonProperty("isRemoteDelete")
val isRemoteDelete: Boolean = false,
@param:JsonProperty("deletedBy")
val deletedBy: String? = null,
@field:JsonProperty
@param:JsonProperty("isMessageRequestAccepted")
val isMessageRequestAccepted: Boolean = true,
@@ -2398,8 +2398,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return Extra(isAlbum = true, individualRecipientId = individualRecipient.serialize())
}
fun forRemoteDelete(individualRecipient: RecipientId): Extra {
return Extra(isRemoteDelete = true, individualRecipientId = individualRecipient.serialize())
fun forRemoteDelete(individualRecipient: RecipientId, deletedBy: RecipientId): Extra {
return Extra(deletedBy = deletedBy.serialize(), individualRecipientId = individualRecipient.serialize())
}
fun forMessageRequest(individualRecipient: RecipientId, isHidden: Boolean = false): Extra {

View File

@@ -155,6 +155,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupRel
import org.thoughtcrime.securesms.database.helpers.migration.V299_AddAttachmentMetadataTable
import org.thoughtcrime.securesms.database.helpers.migration.V300_AddKeyTransparencyColumn
import org.thoughtcrime.securesms.database.helpers.migration.V301_RemoveCallLinkEpoch
import org.thoughtcrime.securesms.database.helpers.migration.V302_AddDeletedByColumn
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -316,10 +317,11 @@ object SignalDatabaseMigrations {
298 to V298_DoNotBackupReleaseNotes,
299 to V299_AddAttachmentMetadataTable,
300 to V300_AddKeyTransparencyColumn,
301 to V301_RemoveCallLinkEpoch
301 to V301_RemoveCallLinkEpoch,
302 to V302_AddDeletedByColumn
)
const val DATABASE_VERSION = 301
const val DATABASE_VERSION = 302
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.signal.core.util.Stopwatch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Adds column to messages to track who has deleted a given message
*/
@Suppress("ClassName")
object V302_AddDeletedByColumn : SignalDatabaseMigration {
private val TAG = Log.tag(V302_AddDeletedByColumn::class.java)
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val stopwatch = Stopwatch("migration", decimalPlaces = 2)
db.execSQL("ALTER TABLE message ADD COLUMN deleted_by INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE")
stopwatch.split("add-column")
db.execSQL("UPDATE message SET deleted_by = from_recipient_id WHERE remote_deleted > 0")
stopwatch.split("copy-data")
db.execSQL("ALTER TABLE message DROP COLUMN remote_deleted")
stopwatch.split("drop-column")
db.execSQL("CREATE INDEX message_deleted_by_index ON message (deleted_by)")
stopwatch.split("create-index")
stopwatch.stop(TAG)
}
}

View File

@@ -53,13 +53,13 @@ public class InMemoryMessageRecord extends MessageRecord {
false,
false,
Collections.emptyList(),
false,
0,
false,
-1,
null,
0,
0,
null,
null);
}

View File

@@ -108,12 +108,12 @@ public abstract class MessageRecord extends DisplayRecord {
private final boolean unidentified;
private final List<ReactionRecord> reactions;
private final long serverTimestamp;
private final boolean remoteDelete;
private final long notifiedTimestamp;
private final long receiptTimestamp;
private final MessageId originalMessageId;
private final int revisionNumber;
private final long pinnedUntil;
private final RecipientId deletedBy;
private final MessageExtras messageExtras;
protected Boolean isJumboji = null;
@@ -130,13 +130,13 @@ public abstract class MessageRecord extends DisplayRecord {
boolean hasReadReceipt,
boolean unidentified,
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete,
long notifiedTimestamp,
boolean viewed,
long receiptTimestamp,
@Nullable MessageId originalMessageId,
int revisionNumber,
long pinnedUntil,
@Nullable RecipientId deletedBy,
@Nullable MessageExtras messageExtras)
{
super(body, fromRecipient, toRecipient, dateSent, dateReceived,
@@ -153,12 +153,12 @@ public abstract class MessageRecord extends DisplayRecord {
this.unidentified = unidentified;
this.reactions = reactions;
this.serverTimestamp = dateServer;
this.remoteDelete = remoteDelete;
this.notifiedTimestamp = notifiedTimestamp;
this.receiptTimestamp = receiptTimestamp;
this.originalMessageId = originalMessageId;
this.revisionNumber = revisionNumber;
this.pinnedUntil = pinnedUntil;
this.deletedBy = deletedBy;
this.messageExtras = messageExtras;
}
@@ -785,6 +785,10 @@ public abstract class MessageRecord extends DisplayRecord {
return pinnedUntil;
}
public @Nullable RecipientId getDeletedBy() {
return deletedBy;
}
public boolean isInMemoryMessageRecord() {
return false;
}
@@ -832,7 +836,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
public boolean isRemoteDelete() {
return remoteDelete;
return deletedBy != null;
}
public @NonNull List<ReactionRecord> getReactions() {

View File

@@ -103,7 +103,6 @@ public class MmsMessageRecord extends MessageRecord {
@NonNull List<LinkPreview> linkPreviews,
boolean unidentified,
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete,
boolean mentionsSelf,
long notifiedTimestamp,
boolean viewed,
@@ -121,12 +120,13 @@ public class MmsMessageRecord extends MessageRecord {
int revisionNumber,
boolean isRead,
long pinnedUntil,
@Nullable RecipientId deletedBy,
@Nullable MessageExtras messageExtras)
{
super(id, body, fromRecipient, fromDeviceId, toRecipient,
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt,
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, expireTimerVersion, hasReadReceipt,
unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, messageExtras);
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, messageExtras);
this.slideDeck = slideDeck;
this.quote = quote;
@@ -337,17 +337,17 @@ public class MmsMessageRecord extends MessageRecord {
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
public @NonNull MmsMessageRecord withoutQuote() {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
public @NonNull MmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
@@ -367,34 +367,34 @@ public class MmsMessageRecord extends MessageRecord {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), slideDeck,
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
public @NonNull MmsMessageRecord withCall(@Nullable CallTable.Call call) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
public @NonNull MmsMessageRecord withPoll(@Nullable PollRecord poll) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), poll, getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
}
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {

View File

@@ -209,6 +209,14 @@ public final class ThreadRecord {
}
}
public @NonNull RecipientId getDeletedByRecipientId() {
if (extra != null && extra.getDeletedBy() != null) {
return RecipientId.from(extra.getDeletedBy());
} else {
return RecipientId.UNKNOWN;
}
}
public @NonNull RecipientId getGroupMessageSender() {
RecipientId threadRecipientId = getRecipient().getId();
RecipientId individualRecipientId = getIndividualRecipientId();

View File

@@ -189,7 +189,7 @@ object SignalSymbols {
VIEW_ONCE_DASH('\uE079'),
VIEW_ONCE_VIEWED('\uE07A'),
X('\u00D7'),
X_CIRCLE('\u2297'),
X_CIRCLE('\uE1EE'),
X_SQUARE('\u2327'),
REFRESH('\uE000'),

View File

@@ -0,0 +1,184 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.models.ServiceId
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.Log.tag
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.protos.AdminDeleteJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.GroupSendUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.util.GroupUtil
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Companion.newBuilder
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.days
/**
* Job used when an admin deletes a message in a group
*/
class AdminDeleteSendJob private constructor(
private val messageId: Long,
private val recipientIds: MutableList<Long>,
private val initialRecipientCount: Int,
parameters: Parameters
) : Job(parameters) {
companion object {
const val KEY: String = "AdminDeleteSendJob"
private val TAG = tag(AdminDeleteSendJob::class.java)
@JvmStatic
fun create(messageId: Long): AdminDeleteSendJob? {
val message = SignalDatabase.messages.getMessageRecord(messageId)
val conversationRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)
if (conversationRecipient == null) {
return null
}
val recipientIds = conversationRecipient.participantIds.map { it.toLong() }.toMutableList()
return AdminDeleteSendJob(
messageId = messageId,
recipientIds = recipientIds,
initialRecipientCount = recipientIds.size,
parameters = Parameters.Builder()
.setQueue(conversationRecipient.id.toQueueKey())
.setLifespan(1.days.inWholeMilliseconds)
.setMaxAttempts(Parameters.UNLIMITED)
.build()
)
}
}
override fun serialize(): ByteArray? {
return AdminDeleteJobData(messageId, recipientIds, initialRecipientCount).encode()
}
override fun getFactoryKey(): String {
return KEY
}
override fun run(): Result {
if (!SignalStore.account.isRegistered) {
Log.w(TAG, "Not registered. Skipping.")
return Result.failure()
}
val message = SignalDatabase.messages.getMessageRecord(messageId)
if (!message.fromRecipient.hasServiceId) {
Log.w(TAG, "Missing service id for the target author.")
return Result.failure()
}
val recipients = recipientIds.map { Recipient.resolved(RecipientId.from(it)) }.toMutableList()
val targetSentTimestamp = message.dateSent
val targetAuthor = message.fromRecipient.requireServiceId()
val conversationRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)
if (conversationRecipient == null) {
Log.w(TAG, "We have a message, but couldn't find the thread!")
return Result.failure()
}
if (!conversationRecipient.isPushV2Group) {
Log.w(TAG, "Cannot admin delete in a non V2 group.")
return Result.failure()
}
val groupRecord = SignalDatabase.groups.getGroup(conversationRecipient.requireGroupId())
if (groupRecord.isEmpty || !groupRecord.get().isAdmin(Recipient.self())) {
Log.w(TAG, "Cannot delete because you are not an admin.")
return Result.failure()
}
val eligible = RecipientUtil.getEligibleForSending(recipients.filter { it.hasServiceId })
val skippedRecipients = recipients - eligible
val sendResult = deliver(conversationRecipient, eligible, targetAuthor, targetSentTimestamp)
for (completion in sendResult.completed) {
recipientIds.remove(completion.id.toLong())
}
for (unregistered in sendResult.unregistered) {
SignalDatabase.recipients.markUnregistered(unregistered)
}
for (recipient in skippedRecipients) {
recipientIds.remove(recipient.id.toLong())
}
Log.i(TAG, "Completed now: ${sendResult.completed.size} Skipped: ${skippedRecipients.size + sendResult.skipped.size} Remaining: ${recipientIds.size}")
if (recipientIds.isEmpty()) {
return Result.success()
} else {
Log.w(TAG, "Still need to send to ${recipients.size} recipients. Retrying.")
return Result.retry(defaultBackoff())
}
}
override fun onFailure() {
Log.w(TAG, "Failed to send admin delete to all recipients! ${initialRecipientCount - recipientIds.size} / $initialRecipientCount")
}
private fun deliver(
conversationRecipient: Recipient,
destinations: MutableList<Recipient>,
targetAuthor: ServiceId,
targetSentTimestamp: Long
): GroupSendJobHelper.SendResult {
val dataMessageBuilder = newBuilder()
.withTimestamp(System.currentTimeMillis())
.withAdminDelete(SignalServiceDataMessage.AdminDelete(targetAuthor, targetSentTimestamp))
GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush())
val nonSelfDestinations = destinations.filterNot { it.isSelf }
val includeSelf = destinations.size != nonSelfDestinations.size
val dataMessage = dataMessageBuilder.build()
val results = GroupSendUtil.sendResendableDataMessage(
context,
conversationRecipient.groupId.map { it.requireV2() }.getOrNull(),
null,
nonSelfDestinations,
false,
ContentHint.RESENDABLE,
MessageId(messageId),
dataMessage,
true,
false,
null,
null
).toMutableList()
if (includeSelf) {
results.add(AppDependencies.signalServiceMessageSender.sendSyncMessage(dataMessage))
}
return GroupSendJobHelper.getCompletedSends(destinations, results)
}
class Factory : Job.Factory<AdminDeleteSendJob?> {
override fun create(parameters: Parameters, serializedData: ByteArray?): AdminDeleteSendJob {
val data = AdminDeleteJobData.ADAPTER.decode(serializedData!!)
return AdminDeleteSendJob(
messageId = data.messageId,
recipientIds = data.recipientIds.toMutableList(),
initialRecipientCount = data.initialRecipientCount,
parameters = parameters
)
}
}
}

View File

@@ -127,6 +127,7 @@ public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
return new HashMap<>() {{
put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory());
put(AdminDeleteSendJob.KEY, new AdminDeleteSendJob.Factory());
put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory());
put(ApkUpdateJob.KEY, new ApkUpdateJob.Factory());
put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory());

View File

@@ -185,6 +185,7 @@ object DataMessageProcessor {
message.pollVote != null -> messageId = handlePollVote(context, envelope, message, senderRecipient, earlyMessageCacheEntry)
message.pinMessage != null -> insertResult = handlePinMessage(envelope, metadata, message, senderRecipient, threadRecipient, groupId, receivedTime, earlyMessageCacheEntry)
message.unpinMessage != null -> messageId = handleUnpinMessage(envelope, message, senderRecipient, threadRecipient, earlyMessageCacheEntry)
message.adminDelete != null -> messageId = handleAdminRemoteDelete(envelope, message, senderRecipient, threadRecipient, earlyMessageCacheEntry)
}
messageId = messageId ?: insertResult?.messageId?.let { MessageId(it) }
@@ -598,7 +599,7 @@ object DataMessageProcessor {
val targetMessage: MessageRecord? = SignalDatabase.messages.getMessageFor(targetSentTimestamp, senderRecipientId)
return if (targetMessage != null && MessageConstraintsUtil.isValidRemoteDeleteReceive(targetMessage, senderRecipientId, envelope.serverTimestamp!!)) {
SignalDatabase.messages.markAsRemoteDelete(targetMessage)
SignalDatabase.messages.markAsRemoteDelete(targetMessage, senderRecipientId)
if (targetMessage.isStory()) {
SignalDatabase.messages.deleteRemotelyDeletedStory(targetMessage.id)
}
@@ -1406,6 +1407,48 @@ object DataMessageProcessor {
return MessageId(targetMessageId)
}
fun handleAdminRemoteDelete(envelope: Envelope, message: DataMessage, senderRecipient: Recipient, threadRecipient: Recipient, earlyMessageCacheEntry: EarlyMessageCacheEntry?): MessageId? {
if (!RemoteConfig.receiveAdminDelete) {
log(envelope.timestamp!!, "Admin delete is not allowed due to remote config.")
return null
}
val delete = message.adminDelete!!
log(envelope.timestamp!!, "Admin delete for message ${delete.targetSentTimestamp}")
val targetSentTimestamp: Long = delete.targetSentTimestamp!!
val targetAuthorServiceId: ServiceId = ACI.parseOrThrow(delete.targetAuthorAciBinary!!)
if (targetAuthorServiceId.isUnknown) {
warn(envelope.timestamp!!, "[handleAdminRemoteDelete] Invalid author.")
return null
}
val targetAuthor = Recipient.externalPush(targetAuthorServiceId)
val targetMessage: MessageRecord? = SignalDatabase.messages.getMessageFor(targetSentTimestamp, targetAuthor.id)
val groupRecord = SignalDatabase.groups.getGroup(threadRecipient.id).orNull()
if (groupRecord == null || !groupRecord.isV2Group) {
warn(envelope.timestamp!!, "[handleAdminRemoteDelete] Invalid group.")
return null
}
return if (targetMessage != null && MessageConstraintsUtil.isValidAdminDeleteReceive(targetMessage, senderRecipient, envelope.serverTimestamp!!, groupRecord)) {
SignalDatabase.messages.markAsRemoteDelete(targetMessage, senderRecipient.id)
MessageId(targetMessage.id)
} else if (targetMessage == null) {
warn(envelope.timestamp!!, "[handleAdminRemoteDelete] Could not find matching message! timestamp: $targetSentTimestamp")
if (earlyMessageCacheEntry != null) {
AppDependencies.earlyMessageCache.store(targetAuthor.id, targetSentTimestamp, earlyMessageCacheEntry)
PushProcessEarlyMessagesJob.enqueue()
}
null
} else {
warn(envelope.timestamp!!, "[handleAdminRemoteDelete] Invalid admin delete! deleteTime: ${envelope.serverTimestamp!!}, targetTime: ${targetMessage.serverTimestamp}, deleteAuthor: ${senderRecipient.id}, targetAuthor: ${targetMessage.fromRecipient.id}, isAdmin: ${groupRecord.isAdmin(senderRecipient)}")
null
}
}
fun notifyTypingStoppedFromIncomingMessage(context: Context, senderRecipient: Recipient, threadRecipientId: RecipientId, device: Int) {
val threadId = SignalDatabase.threads.getThreadIdIfExistsFor(threadRecipientId)

View File

@@ -130,6 +130,8 @@ open class MessageContentProcessor(private val context: Context) {
getGroupRecipient(content.dataMessage?.groupV2, sender)
} else if (content.editMessage?.dataMessage.hasGroupContext) {
getGroupRecipient(content.editMessage?.dataMessage?.groupV2, sender)
} else if (content.syncMessage?.sent?.message.hasGroupContext) {
getGroupRecipient(content.syncMessage?.sent?.message?.groupV2, sender)
} else {
sender
}

View File

@@ -56,7 +56,8 @@ object SignalServiceProtoUtil {
pollVote != null ||
pollTerminate != null ||
pinMessage != null ||
unpinMessage != null
unpinMessage != null ||
adminDelete != null
}
val DataMessage.hasDisallowedAnnouncementOnlyContent: Boolean

View File

@@ -258,6 +258,10 @@ object SyncMessageProcessor {
DataMessageProcessor.handleUnpinMessage(envelope, dataMessage, senderRecipient, threadRecipient, earlyMessageCacheEntry)
threadId = SignalDatabase.threads.getOrCreateThreadIdFor(getSyncMessageDestination(sent))
}
dataMessage.adminDelete != null -> {
DataMessageProcessor.handleAdminRemoteDelete(envelope, dataMessage, senderRecipient, threadRecipient, earlyMessageCacheEntry)
threadId = SignalDatabase.threads.getOrCreateThreadIdFor(getSyncMessageDestination(sent))
}
else -> threadId = handleSynchronizeSentTextMessage(sent, envelope.timestamp!!)
}

View File

@@ -193,9 +193,10 @@ public class ApplicationMigrations {
static final int ATTACHMENT_HASH_BACKFILL_2 = 149;
static final int SVR2_ENCLAVE_UPDATE_5 = 150;
static final int STICKER_PACK_ADDITION_2 = 151;
static final int DELETED_BY_DB_MIGRATION = 152;
}
public static final int CURRENT_VERSION = 151;
public static final int CURRENT_VERSION = 152;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@@ -894,6 +895,10 @@ public class ApplicationMigrations {
jobs.put(Version.STICKER_PACK_ADDITION_2, new StickerPackAddition2MigrationJob());
}
if (lastSeenVersion < Version.DELETED_BY_DB_MIGRATION) {
jobs.put(Version.DELETED_BY_DB_MIGRATION, new DatabaseMigrationJob());
}
return jobs;
}

View File

@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AdminDeleteSendJob;
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
@@ -78,7 +79,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -507,7 +507,7 @@ public class MessageSender {
public static void sendRemoteDelete(long messageId) {
MessageTable db = SignalDatabase.messages();
db.markAsRemoteDelete(messageId);
db.markAsDeleteBySelf(messageId);
db.markAsSending(messageId);
try {
@@ -518,6 +518,17 @@ public class MessageSender {
}
}
public static void sendAdminDelete(long messageId) {
// TODO(michelle): Update with failure states
SignalDatabase.messages().markAsDeleteBySelf(messageId);
AdminDeleteSendJob job = AdminDeleteSendJob.create(messageId);
if (job != null) {
AppDependencies.getJobManager().add(job);
} else {
Log.w(TAG, "[sendAdminDelete] Could not create the admin delete job.");
}
}
public static void resendGroupMessage(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set<RecipientId> filterRecipientIds) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
sendGroupPush(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());

View File

@@ -32,7 +32,8 @@ object DeleteDialog {
messageRecords: Set<MessageRecord>,
title: CharSequence? = null,
message: CharSequence = context.resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageRecords.size, messageRecords.size),
forceRemoteDelete: Boolean = false
forceRemoteDelete: Boolean = false,
isAdmin: Boolean = false
): Single<Pair<Boolean, Boolean>> = Single.create { emitter ->
val builder = MaterialAlertDialogBuilder(context)
@@ -43,7 +44,7 @@ object DeleteDialog {
val isNoteToSelfDelete = isNoteToSelfDelete(messageRecords)
if (forceRemoteDelete) {
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords, emitter) }
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords = messageRecords, isAdminDelete = false, emitter = emitter) }
} else {
val positiveButton = if (isNoteToSelfDelete) {
R.string.ConversationFragment_delete
@@ -58,7 +59,9 @@ object DeleteDialog {
}
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && !isNoteToSelfDelete) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context = context, messageRecords = messageRecords, isAdminDelete = false, emitter = emitter) }
} else if (MessageConstraintsUtil.isValidAdminDeleteSend(messageRecords, System.currentTimeMillis(), isAdmin)) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleAdminDeleteForEveryone(context, messageRecords, emitter) }
}
}
@@ -71,15 +74,27 @@ object DeleteDialog {
return messageRecords.all { messageRecord: MessageRecord -> messageRecord.isOutgoing && messageRecord.toRecipient.isSelf }
}
private fun handleDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
private fun handleAdminDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
MaterialAlertDialogBuilder(context)
.setTitle("${context.getString(R.string.ConversationFragment_delete_for_everyone_title)} - INTERNAL ONLY")
.setMessage(R.string.ConversationFragment_delete_for_everyone_body)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
handleDeleteForEveryone(context = context, messageRecords = messageRecords, isAdminDelete = true, emitter = emitter)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(Pair(false, false)) }
.setOnCancelListener { emitter.onSuccess(Pair(false, false)) }
.show()
}
private fun handleDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, isAdminDelete: Boolean, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
if (SignalStore.uiHints.hasConfirmedDeleteForEveryoneOnce()) {
deleteForEveryone(messageRecords, emitter)
deleteForEveryone(messageRecords, isAdminDelete, emitter)
} else {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
SignalStore.uiHints.markHasConfirmedDeleteForEveryoneOnce()
deleteForEveryone(messageRecords, emitter)
deleteForEveryone(messageRecords, isAdminDelete, emitter)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(Pair(false, false)) }
.setOnCancelListener { emitter.onSuccess(Pair(false, false)) }
@@ -87,10 +102,14 @@ object DeleteDialog {
}
}
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, isAdminDelete: Boolean, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
SignalExecutors.BOUNDED.execute {
messageRecords.forEach { message ->
MessageSender.sendRemoteDelete(message.id)
if (isAdminDelete) {
MessageSender.sendAdminDelete(message.id)
} else {
MessageSender.sendRemoteDelete(message.id)
}
}
emitter.onSuccess(Pair(true, false))

View File

@@ -1,10 +1,11 @@
package org.thoughtcrime.securesms.util
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -12,8 +13,10 @@ import kotlin.time.Duration.Companion.milliseconds
* have strict time limits.
*/
object MessageConstraintsUtil {
private val RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(2)
private val SEND_THRESHOLD = TimeUnit.DAYS.toMillis(1)
private val SEND_THRESHOLD = RemoteConfig.regularDeleteThreshold.milliseconds.inWholeMilliseconds
private val RECEIVE_THRESHOLD = SEND_THRESHOLD + 1.days.inWholeMilliseconds
private val ADMIN_SEND_THRESHOLD = RemoteConfig.adminDeleteThreshold.milliseconds.inWholeMilliseconds
private val ADMIN_RECEIVE_THRESHOLD = ADMIN_SEND_THRESHOLD + 1.days.inWholeMilliseconds
const val MAX_EDIT_COUNT = 10
@@ -31,6 +34,14 @@ object MessageConstraintsUtil {
((deleteServerTimestamp - messageTimestamp < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing))
}
@JvmStatic
fun isValidAdminDeleteReceive(targetMessage: MessageRecord, deleteSender: Recipient, deleteServerTimestamp: Long, groupRecord: GroupRecord): Boolean {
val isValidSender = groupRecord.isAdmin(deleteSender)
val messageTimestamp = targetMessage.dateSent
return isValidSender && (deleteServerTimestamp - messageTimestamp < ADMIN_RECEIVE_THRESHOLD)
}
@JvmStatic
fun isValidEditMessageReceive(targetMessage: MessageRecord, editSender: Recipient, editServerTimestamp: Long): Boolean {
return isValidRemoteDeleteReceive(targetMessage, editSender.id, editServerTimestamp)
@@ -42,6 +53,11 @@ object MessageConstraintsUtil {
return targetMessages.all { isValidRemoteDeleteSend(it, currentTime) }
}
@JvmStatic
fun isValidAdminDeleteSend(targetMessages: Collection<MessageRecord>, currentTime: Long, isAdmin: Boolean): Boolean {
return targetMessages.all { isValidAdminDeleteSend(it, currentTime, isAdmin) }
}
@JvmStatic
fun isWithinMaxEdits(targetMessage: MessageRecord): Boolean {
return targetMessage.revisionNumber < MAX_EDIT_COUNT
@@ -94,6 +110,19 @@ object MessageConstraintsUtil {
(currentTime - message.dateSent < SEND_THRESHOLD || message.toRecipient.isSelf)
}
private fun isValidAdminDeleteSend(message: MessageRecord, currentTime: Long, isAdmin: Boolean): Boolean {
return RemoteConfig.sendAdminDelete &&
isAdmin &&
!message.isUpdate &&
message.isPush &&
(!message.toRecipient.isGroup || message.toRecipient.isActiveGroup) &&
!message.isRemoteDelete &&
!message.hasGiftBadge() &&
!message.isPaymentNotification &&
!message.isPaymentTombstone &&
(currentTime - message.dateSent < ADMIN_SEND_THRESHOLD)
}
private fun isSelf(recipientId: RecipientId): Boolean {
return Recipient.isSelfSet && Recipient.self().id == recipientId
}

View File

@@ -1257,5 +1257,48 @@ object RemoteConfig {
hotSwappable = true
)
/**
* Whether or not to receive admin delete messages.
*/
@JvmStatic
@get:JvmName("receiveAdminDelete")
val receiveAdminDelete: Boolean by remoteBoolean(
key = "android.receiveAdminDelete",
defaultValue = false,
hotSwappable = true
)
/**
* Whether or not to send admin delete messages.
*/
@JvmStatic
@get:JvmName("sendAdminDelete")
val sendAdminDelete: Boolean by remoteBoolean(
key = "android.sendAdminDelete",
defaultValue = false,
hotSwappable = true
)
/**
* Maximum time that passes where a message can still be regularly deleted
*/
@JvmStatic
@get:JvmName("regularDeleteThreshold")
val regularDeleteThreshold: Long by remoteLong(
key = "android.regularDeleteThreshold",
defaultValue = 1.days.inWholeMilliseconds,
hotSwappable = true
)
/**
* Maximum time that passes where a message can still be deleted by an admin
*/
@JvmStatic
@get:JvmName("adminDeleteThreshold")
val adminDeleteThreshold: Long by remoteLong(
key = "android.adminDeleteThreshold",
defaultValue = 1.days.inWholeMilliseconds,
hotSwappable = true
)
// endregion
}