From a445e0a3264cb5cc1d1ab3e0b7afb6a452e47b91 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 30 Mar 2021 17:13:25 +1100 Subject: [PATCH] fix: moderator status going off open chat API instead of PublicChatAPI --- .../conversation/ConversationItem.java | 29 +-- .../securesms/loki/api/PublicChatPoller.kt | 238 ++++++++++++++++++ .../MessageReceiverHandler.kt | 2 +- .../pollers/OpenGroupPoller.kt | 6 +- 4 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 650a708e53..44f93c8332 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -52,11 +52,21 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; - +import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; +import org.session.libsession.messaging.threads.recipients.Recipient; +import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; +import org.session.libsession.utilities.GroupUtil; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.ThemeUtil; +import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.ViewUtil; +import org.session.libsession.utilities.views.Stub; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.loki.api.opengroups.PublicChat; -import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; +import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.MediaPreviewActivity; @@ -69,7 +79,6 @@ import org.thoughtcrime.securesms.components.LinkPreviewView; import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.StickerView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -78,7 +87,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; -import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; import org.thoughtcrime.securesms.loki.views.MessageAudioView; import org.thoughtcrime.securesms.loki.views.ProfilePictureView; @@ -89,22 +97,11 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.TextSlide; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.LongClickCopySpan; import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.SearchUtil; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.ThemeUtil; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.GroupUtil; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.views.Stub; - import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -916,7 +913,7 @@ public class ConversationItem extends LinearLayout PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId()); if (publicChat != null) { - boolean isModerator = PublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer()); + boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt new file mode 100644 index 0000000000..87c0e29570 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt @@ -0,0 +1,238 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.Context +import android.os.Handler +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.recipients.Recipient +import org.session.libsession.utilities.IdentityKeyUtil +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.libsignal.util.guava.Optional +import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer +import org.session.libsignal.service.api.messages.SignalServiceContent +import org.session.libsignal.service.api.messages.SignalServiceDataMessage +import org.session.libsignal.service.api.messages.SignalServiceGroup +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.loki.api.fileserver.FileServerAPI +import org.session.libsignal.service.loki.api.opengroups.PublicChat +import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI +import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage +import org.session.libsignal.utilities.logging.Log +import org.session.libsignal.utilities.successBackground +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.jobs.PushDecryptJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import java.security.MessageDigest +import java.util.* + +class PublicChatPoller(private val context: Context, private val group: PublicChat) { + private val handler by lazy { Handler() } + private var hasStarted = false + private var isPollOngoing = false + public var isCaughtUp = false + + // region Convenience + private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)!! + private var displayNameUpdatees = setOf() + + private val api: PublicChatAPI + get() = { + val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() + val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context) + val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context) + val openGroupDatabase = DatabaseFactory.getGroupDatabase(context) + PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase) + }() + // endregion + + // region Tasks + private val pollForNewMessagesTask = object : Runnable { + + override fun run() { + pollForNewMessages() + handler.postDelayed(this, pollForNewMessagesInterval) + } + } + + private val pollForDeletedMessagesTask = object : Runnable { + + override fun run() { + pollForDeletedMessages() + handler.postDelayed(this, pollForDeletedMessagesInterval) + } + } + + private val pollForModeratorsTask = object : Runnable { + + override fun run() { + pollForModerators() + handler.postDelayed(this, pollForModeratorsInterval) + } + } + + private val pollForDisplayNamesTask = object : Runnable { + + override fun run() { + pollForDisplayNames() + handler.postDelayed(this, pollForDisplayNamesInterval) + } + } + // endregion + + // region Settings + companion object { + private val pollForNewMessagesInterval: Long = 4 * 1000 + private val pollForDeletedMessagesInterval: Long = 60 * 1000 + private val pollForModeratorsInterval: Long = 10 * 60 * 1000 + private val pollForDisplayNamesInterval: Long = 60 * 1000 + } + // endregion + + // region Lifecycle + fun startIfNeeded() { + if (hasStarted) return + pollForNewMessagesTask.run() + pollForDeletedMessagesTask.run() + pollForModeratorsTask.run() + pollForDisplayNamesTask.run() + hasStarted = true + } + + fun stop() { + handler.removeCallbacks(pollForNewMessagesTask) + handler.removeCallbacks(pollForDeletedMessagesTask) + handler.removeCallbacks(pollForModeratorsTask) + handler.removeCallbacks(pollForDisplayNamesTask) + hasStarted = false + } + // endregion + + // region Polling + private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage { + val id = group.id.toByteArray() + val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null) + val quote = if (message.quote != null) { + SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf()) + } else { + null + } + val attachments = message.attachments.mapNotNull { attachment -> + if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null } + SignalServiceAttachmentPointer( + attachment.serverID, + attachment.contentType, + ByteArray(0), + Optional.of(attachment.size), + Optional.absent(), + attachment.width, attachment.height, + Optional.absent(), + Optional.of(attachment.fileName), + false, + Optional.fromNullable(attachment.caption), + attachment.url) + } + val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview } + val signalLinkPreviews = mutableListOf() + if (linkPreview != null) { + val attachment = SignalServiceAttachmentPointer( + linkPreview.serverID, + linkPreview.contentType, + ByteArray(0), + Optional.of(linkPreview.size), + Optional.absent(), + linkPreview.width, linkPreview.height, + Optional.absent(), + Optional.of(linkPreview.fileName), + false, + Optional.fromNullable(linkPreview.caption), + linkPreview.url) + signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment))) + } + val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body + val syncTarget = if (message.senderPublicKey == userHexEncodedPublicKey) group.id else null + return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, 0, false, null, quote, null, signalLinkPreviews, null, syncTarget) + } + + fun pollForNewMessages(): Promise { + if (isPollOngoing) { return Promise.of(Unit) } + isPollOngoing = true + val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB) + // Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below + val promise = api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages -> + Promise.of(messages) + } + promise.successBackground { messages -> + // Process messages in the background + messages.forEach { message -> + // If the sender of the current message is not a slave device, set the display name in the database + val senderDisplayName = "${message.displayName} (...${message.senderPublicKey.takeLast(8)})" + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.senderPublicKey, senderDisplayName) + val senderHexEncodedPublicKey = message.senderPublicKey + val serviceDataMessage = getDataMessage(message) + val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.serverTimestamp, false) + if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) { + PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) + } else { + PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) + } + // Update profile picture if needed + val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false) + if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) { + val profileKey = message.profilePicture!!.profileKey + val url = message.profilePicture!!.url + if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) { + val database = DatabaseFactory.getRecipientDatabase(context) + database.setProfileKey(senderAsRecipient, profileKey) + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url)) + } + } + } + isCaughtUp = true + isPollOngoing = false + } + promise.fail { + Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.") + isPollOngoing = false + } + return promise.map { Unit } + } + + private fun pollForDisplayNames() { + if (displayNameUpdatees.isEmpty()) { return } + val hexEncodedPublicKeys = displayNameUpdatees + displayNameUpdatees = setOf() + api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping -> + for (pair in mapping.entries) { + val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})" + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName) + } + }.fail { + displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys) + } + } + + private fun pollForDeletedMessages() { + api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs -> + val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) + val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) } + val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context) + val mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context) + deletedMessageIDs.forEach { + smsMessageDatabase.deleteMessage(it) + mmsMessageDatabase.delete(it) + } + }.fail { + Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.") + } + } + + private fun pollForModerators() { + api.getModerators(group.channel, group.server) + } + // endregion +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 8efcbd8806..6e42ec23c3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -130,7 +130,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS val context = MessagingConfiguration.shared.context // Update profile if needed val newProfile = message.profile - if (newProfile != null && openGroupID.isNullOrEmpty()) { + if (newProfile != null) { val profileManager = SSKEnvironment.shared.profileManager val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false) val displayName = newProfile.displayName!! diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 5bbae93ca6..389dc370a8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -83,7 +83,10 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ messages.forEach { message -> try { val senderPublicKey = message.senderPublicKey - val senderDisplayName = message.displayName + fun generateDisplayName(rawDisplayName: String): String { + return "$rawDisplayName (...${senderPublicKey.takeLast(8)})" + } + val senderDisplayName = MessagingConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName(message.displayName) val id = openGroup.id.toByteArray() // Main message val dataMessageProto = DataMessage.newBuilder() @@ -187,6 +190,7 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ Log.e("Loki", "Exception parsing message", e) } } + displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey isCaughtUp = true isPollOngoing = false deferred.resolve(Unit)