mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-30 13:41:47 +01:00
Add support for admin delete.
This commit is contained in:
committed by
Cody Henthorne
parent
1968438ebb
commit
071fbfd916
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,13 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||
false,
|
||||
false,
|
||||
Collections.emptyList(),
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
-1,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ object SignalServiceProtoUtil {
|
||||
pollVote != null ||
|
||||
pollTerminate != null ||
|
||||
pinMessage != null ||
|
||||
unpinMessage != null
|
||||
unpinMessage != null ||
|
||||
adminDelete != null
|
||||
}
|
||||
|
||||
val DataMessage.hasDisallowedAnnouncementOnlyContent: Boolean
|
||||
|
||||
@@ -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!!)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user