From 0eadc55325ec7d0d626a37311d5184bcd0feee97 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 13 Apr 2021 17:17:16 +1000 Subject: [PATCH] feat: add open group v2 storage and db methods, starting on new open group v2 poller --- .../securesms/database/Storage.kt | 53 +++++ .../securesms/loki/api/PublicChatManager.kt | 2 +- .../loki/database/LokiAPIDatabase.kt | 51 ++++ .../loki/database/LokiThreadDatabase.kt | 38 ++- .../loki/protocol/MultiDeviceProtocol.kt | 89 ------- .../libsession/messaging/StorageProtocol.kt | 50 ++-- .../messaging/opengroups/OpenGroupAPIV2.kt | 37 +++ .../messaging/opengroups/OpenGroupV2.kt | 45 ++++ .../MessageReceiverHandler.kt | 2 + .../pollers/OpenGroupV2Poller.kt | 225 ++++++++++++++++++ .../service/loki/api/opengroups/PublicChat.kt | 1 + .../loki/database/LokiAPIDatabaseProtocol.kt | 5 + 12 files changed, 484 insertions(+), 114 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 50d7232710..62caac50f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -14,6 +14,7 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup +import org.session.libsession.messaging.opengroups.OpenGroupV2 import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview @@ -221,6 +222,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null) } + override fun getAuthToken(room: String, server: String): String? { + val id = "$server.$room" + return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id) + } + + override fun setAuthToken(room: String, server: String, newValue: String) { + val id = "$server.$room" + DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, newValue) + } + + override fun removeAuthToken(room: String, server: String) { + val id = "$server.$room" + DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null) + } + override fun getOpenGroup(threadID: String): OpenGroup? { if (threadID.toInt() < 0) { return null } val database = databaseHelper.readableDatabase @@ -230,6 +246,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun getV2OpenGroup(threadId: String): OpenGroupV2? { + if (threadId.toInt() < 0) { return null } + val database = databaseHelper.readableDatabase + return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor -> + val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) + OpenGroupV2.fromJson(publicChatAsJson) + } + } + override fun getThreadID(openGroupID: String): String { val address = Address.fromSerialized(openGroupID) val recipient = Recipient.from(context, address, false) @@ -254,6 +279,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey) } + override fun getLastMessageServerId(room: String, server: String): Long? { + return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server) + } + + override fun setLastMessageServerId(room: String, server: String, newValue: Long) { + DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue) + } + + override fun removeLastMessageServerId(room: String, server: String) { + DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server) + } + override fun getLastMessageServerID(group: Long, server: String): Long? { return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server) } @@ -266,6 +303,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server) } + override fun getLastDeletionServerId(room: String, server: String): Long? { + TODO("Not yet implemented") + } + + override fun setLastDeletionServerId(room: String, server: String, newValue: Long) { + TODO("Not yet implemented") + } + + override fun removeLastDeletionServerId(room: String, server: String) { + TODO("Not yet implemented") + } + override fun getLastDeletionServerID(group: Long, server: String): Long? { return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server) } @@ -471,6 +520,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun getAllV2OpenGroups(): Map { + return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups() + } + override fun addOpenGroup(server: String, channel: Long) { OpenGroupUtilities.addGroup(context, server, channel) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index c2200ac0a7..ef36d98de9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -24,7 +24,7 @@ class PublicChatManager(private val context: Context) { private val pollers = mutableMapOf() private val observers = mutableMapOf() private var isPolling = false - private val executorService = Executors.newScheduledThreadPool(16) + private val executorService = Executors.newScheduledThreadPool(4) public fun areAllCaughtUp(): Boolean { var areAllCaughtUp = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index a9325a5527..e4ea2d0098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -286,6 +286,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( }?.toLong() } + override fun getLastMessageServerID(room: String, server: String): Long? { + val database = databaseHelper.writableDatabase + val index = "$server.$room" + return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor -> + cursor.getInt(lastMessageServerID) + }?.toLong() + } + override fun setLastMessageServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" @@ -293,12 +301,25 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index)) } + override fun setLastMessageServerID(room: String, server: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val index = "$server.$room" + val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() )) + database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index)) + } + fun removeLastMessageServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index)) } + fun removeLastMessageServerID(room: String, server:String) { + val database = databaseHelper.writableDatabase + val index = "$server.$channel" + database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) + } + override fun getLastDeletionServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" @@ -307,6 +328,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( }?.toLong() } + override fun getLastDeletionServerID(room: String, server: String): Long? { + val database = databaseHelper.readableDatabase + val index = "$server.$room" + return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor -> + cursor.getInt(lastDeletionServerID) + }?.toLong() + } + override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" @@ -314,6 +343,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } + override fun setLastDeletionServerID(room: String, server: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val index = "$server.$room" + val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString())) + database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) + } + fun removeLastDeletionServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" @@ -328,6 +364,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( }?.toInt() } + fun getUserCount(room: String, server: String): Int? { + val database = databaseHelper.readableDatabase + val index = "$server.$room" + return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor -> + cursor.getInt(userCount) + }?.toInt() + } + override fun setUserCount(group: Long, server: String, newValue: Int) { val database = databaseHelper.writableDatabase val index = "$server.$group" @@ -335,6 +379,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) } + override fun setUserCount(room: String, server: String, newValue: Int) { + val database = databaseHelper.writableDatabase + val index = "$server.$room" + val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() )) + database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) + } + override fun getSessionRequestSentTimestamp(publicKey: String): Long? { val database = databaseHelper.readableDatabase return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt index 0c2dff9a80..60057f606d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt @@ -3,20 +3,18 @@ package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context import android.database.Cursor - -import org.thoughtcrime.securesms.database.Database -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.loki.utilities.* - +import org.session.libsession.messaging.opengroups.OpenGroupV2 import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.recipients.Recipient -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.service.loki.api.opengroups.PublicChat - -import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol -import org.session.libsignal.service.loki.utilities.PublicKeyValidation +import org.session.libsignal.utilities.JsonUtil +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.loki.utilities.get +import org.thoughtcrime.securesms.loki.utilities.getString +import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol { @@ -57,6 +55,26 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return result } + fun getAllV2OpenGroups(): Map { + val database = databaseHelper.readableDatabase + var cursor: Cursor? = null + val result = mutableMapOf() + try { + cursor = database.rawQuery("select * from $publicChatTable", null) + while (cursor != null && cursor.moveToNext()) { + val threadID = cursor.getLong(threadID) + val string = cursor.getString(publicChat) + val openGroup = OpenGroupV2.fromJson(string) + if (openGroup != null) result[threadID] = openGroup + } + } catch (e: Exception) { + // do nothing + } finally { + cursor?.close() + } + return result + } + fun getAllPublicChatServers(): Set { return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt index 41b3de72f8..b58f608502 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -1,25 +1,12 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context -import com.google.protobuf.ByteString -import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.threads.Address -import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.preferences.ProfileKeyUtil -import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage -import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Hex -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.loki.utilities.ContactUtilities -import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities object MultiDeviceProtocol { @@ -51,80 +38,4 @@ object MultiDeviceProtocol { TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) } - // TODO: remove this after we migrate to new message receiving pipeline - @JvmStatic - fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) { - synchronized(this) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return - if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, timestamp)) return - if (senderPublicKey != userPublicKey) return - TextSecurePreferences.setConfigurationMessageSynced(context, true) - TextSecurePreferences.setLastProfileUpdateTime(context, timestamp) - - val configurationMessage = ConfigurationMessage.fromProto(content) ?: return - - val storage = MessagingConfiguration.shared.storage - val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() - - val threadDatabase = DatabaseFactory.getThreadDatabase(context) - val recipientDatabase = DatabaseFactory.getRecipientDatabase(context) - - val ourRecipient = Recipient.from(context, Address.fromSerialized(userPublicKey),false) - - for (closedGroup in configurationMessage.closedGroups) { - if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue - - val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder() - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW - closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey)) - closedGroupUpdate.name = closedGroup.name - val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder() - encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded()) - encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.privateKey.serialize()) - closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build() - closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) - closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) - - ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp) - } - val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } - for (openGroup in configurationMessage.openGroups) { - if (allOpenGroups.contains(openGroup)) continue - OpenGroupUtilities.addGroup(context, openGroup, 1) - } - if (configurationMessage.displayName.isNotEmpty()) { - TextSecurePreferences.setProfileName(context, configurationMessage.displayName) - recipientDatabase.setProfileName(ourRecipient, configurationMessage.displayName) - } - if (configurationMessage.profileKey.isNotEmpty()) { - val profileKey = Base64.encodeBytes(configurationMessage.profileKey) - ProfileKeyUtil.setEncodedProfileKey(context, profileKey) - recipientDatabase.setProfileKey(ourRecipient, configurationMessage.profileKey) - if (!configurationMessage.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != configurationMessage.profilePicture) { - TextSecurePreferences.setProfilePictureURL(context, configurationMessage.profilePicture) - ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, configurationMessage.profilePicture)) - } - } - for (contact in configurationMessage.contacts) { - val address = Address.fromSerialized(contact.publicKey) - val recipient = Recipient.from(context, address, true) - if (!contact.profilePicture.isNullOrEmpty()) { - recipientDatabase.setProfileAvatar(recipient, contact.profilePicture) - } - if (contact.profileKey?.isNotEmpty() == true) { - recipientDatabase.setProfileKey(recipient, contact.profileKey) - } - if (contact.name.isNotEmpty()) { - recipientDatabase.setProfileName(recipient, contact.name) - } - recipientDatabase.setProfileSharing(recipient, true) - recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED) - // create Thread if needed - threadDatabase.getOrCreateThreadIdFor(recipient) - } - if (configurationMessage.contacts.isNotEmpty()) { - threadDatabase.notifyUpdatedFromConfig() - } - } - } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index 64c1828d51..95e5eb680a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -10,6 +10,7 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup +import org.session.libsession.messaging.opengroups.OpenGroupV2 import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview @@ -51,14 +52,16 @@ interface StorageProtocol { fun isJobCanceled(job: Job): Boolean // Authorization - fun getAuthToken(server: String): String? - fun setAuthToken(server: String, newValue: String?) - fun removeAuthToken(server: String) + fun getAuthToken(room: String, server: String): String? + fun setAuthToken(room: String, server: String, newValue: String) + fun removeAuthToken(room: String, server: String) + + // Open Groups + fun getAllV2OpenGroups(): Map + fun getV2OpenGroup(threadId: String): OpenGroupV2? // Open Groups - fun getOpenGroup(threadID: String): OpenGroup? fun getThreadID(openGroupID: String): String? - fun getAllOpenGroups(): Map fun addOpenGroup(server: String, channel: Long) fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) fun getQuoteServerID(quoteID: Long, publicKey: String): Long? @@ -72,21 +75,19 @@ interface StorageProtocol { fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? // Open Group Metadata - fun setUserCount(group: Long, server: String, newValue: Int) - fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) - fun getOpenGroupProfilePictureURL(group: Long, server: String): String? + fun updateTitle(groupID: String, newValue: String) fun updateProfilePicture(groupID: String, newValue: ByteArray) // Last Message Server ID - fun getLastMessageServerID(group: Long, server: String): Long? - fun setLastMessageServerID(group: Long, server: String, newValue: Long) - fun removeLastMessageServerID(group: Long, server: String) + fun getLastMessageServerId(room: String, server: String): Long? + fun setLastMessageServerId(room: String, server: String, newValue: Long) + fun removeLastMessageServerId(room: String, server: String) // Last Deletion Server ID - fun getLastDeletionServerID(group: Long, server: String): Long? - fun setLastDeletionServerID(group: Long, server: String, newValue: Long) - fun removeLastDeletionServerID(group: Long, server: String) + fun getLastDeletionServerId(room: String, server: String): Long? + fun setLastDeletionServerId(room: String, server: String, newValue: Long) + fun removeLastDeletionServerId(room: String, server: String) // Message Handling fun isMessageDuplicated(timestamp: Long, sender: String): Boolean @@ -158,4 +159,25 @@ interface StorageProtocol { // Message Handling /// Returns the ID of the `TSIncomingMessage` that was constructed. fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? + + // DEPRECATED + fun getAuthToken(server: String): String? + fun setAuthToken(server: String, newValue: String?) + fun removeAuthToken(server: String) + + fun getLastMessageServerID(group: Long, server: String): Long? + fun setLastMessageServerID(group: Long, server: String, newValue: Long) + fun removeLastMessageServerID(group: Long, server: String) + + fun getLastDeletionServerID(group: Long, server: String): Long? + fun setLastDeletionServerID(group: Long, server: String, newValue: Long) + fun removeLastDeletionServerID(group: Long, server: String) + + fun getOpenGroup(threadID: String): OpenGroup? + fun getAllOpenGroups(): Map + + fun setUserCount(group: Long, server: String, newValue: Int) + fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) + fun getOpenGroupProfilePictureURL(group: Long, server: String): String? + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt new file mode 100644 index 0000000000..c9bacb0d87 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt @@ -0,0 +1,37 @@ +package org.session.libsession.messaging.opengroups + +import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error +import org.session.libsession.messaging.utilities.DotNetAPI +import java.util.* + +class OpenGroupAPIV2: DotNetAPI() { + + enum class Error { + GENERIC, + PARSING_FAILED, + DECRYPTION_FAILED, + SIGNING_FAILED, + INVALID_URL, + NO_PUBLIC_KEY + } + + companion object { + private val moderators: HashMap>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) + const val DEFAULT_SERVER = "https://sessionopengroup.com" + const val DEFAULT_SERVER_PUBLIC_KEY = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b" + } + + + +} + +data class Info(val id: String, val name: String, val imageId: String?) + +fun Error.errorDescription() = when (this) { + Error.GENERIC -> "An error occurred." + Error.PARSING_FAILED -> "Invalid response." + Error.DECRYPTION_FAILED -> "Couldn't decrypt response." + Error.SIGNING_FAILED -> "Couldn't sign message." + Error.INVALID_URL -> "Invalid URL." + Error.NO_PUBLIC_KEY -> "Couldn't find server public key." +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2.kt new file mode 100644 index 0000000000..db700d3ccc --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2.kt @@ -0,0 +1,45 @@ +package org.session.libsession.messaging.opengroups + +import org.session.libsignal.utilities.JsonUtil +import java.util.* + +data class OpenGroupV2( + val server: String, + val room: String, + val id: String, + val name: String, + val publicKey: String, + val imageId: String? +) { + + constructor(server: String, room: String, name: String, publicKey: String, imageId: String?) : this( + server = server, + room = room, + id = "$server.$room", + name = name, + publicKey = publicKey, + imageId = imageId + ) + + companion object { + + fun fromJson(jsonAsString: String): OpenGroupV2? { + return try { + val json = JsonUtil.fromJson(jsonAsString) + if (!json.has("room")) return null + + val room = json.get("room").asText().toLowerCase(Locale.getDefault()) + val server = json.get("server").asText().toLowerCase(Locale.getDefault()) + val displayName = json.get("displayName").asText() + val publicKey = json.get("publicKey").asText() + val imageId = json.get("imageId").asText().let { str -> if (str.isEmpty()) null else str } + + OpenGroupV2(server, room, displayName, publicKey, imageId) + } catch (e: Exception) { + null + } + } + + } + +} \ 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 bc2546b0a5..9557ddb62e 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 @@ -105,8 +105,10 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } + val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.server } for (openGroup in message.openGroups) { if (allOpenGroups.contains(openGroup)) continue + // TODO: add in v2 storage.addOpenGroup(openGroup, 1) } if (message.displayName.isNotEmpty()) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt new file mode 100644 index 0000000000..21e0c0d0cb --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt @@ -0,0 +1,225 @@ +package org.session.libsession.messaging.sending_receiving.pollers + +import com.google.protobuf.ByteString +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.opengroups.* +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.utilities.logging.Log +import org.session.libsignal.utilities.successBackground +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executorService: ScheduledExecutorService? = null) { + + private var hasStarted = false + @Volatile private var isPollOngoing = false + var isCaughtUp = false + + private val cancellableFutures = mutableListOf>() + + // region Convenience + private val userHexEncodedPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: "" + private var displayNameUpdates = setOf() + // endregion + + // region Settings + companion object { + private val pollForNewMessagesInterval: Long = 10 * 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 || executorService == null) return + cancellableFutures += listOf( + executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS), + executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS), + executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS), + executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS) + ) + hasStarted = true + } + + fun stop() { + cancellableFutures.forEach { future -> + future.cancel(false) + } + cancellableFutures.clear() + hasStarted = false + } + // endregion + + // region Polling + fun pollForNewMessages(): Promise { + return pollForNewMessages(false) + } + + private fun pollForNewMessages(isBackgroundPoll: Boolean): Promise { + if (isPollOngoing) { return Promise.of(Unit) } + isPollOngoing = true + val deferred = deferred() + // Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below + OpenGroupAPIV2.getMessages(openGroup.room, openGroup.server).successBackground { messages -> + // Process messages in the background + Log.d("Loki", "received ${messages.size} messages") + messages.forEach { message -> + try { + val senderPublicKey = message.senderPublicKey + 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 = SignalServiceProtos.DataMessage.newBuilder() + val body = if (message.body == message.timestamp.toString()) { "" } else { message.body } + dataMessageProto.setBody(body) + dataMessageProto.setTimestamp(message.timestamp) + // Attachments + val attachmentProtos = message.attachments.mapNotNull { attachment -> + try { + if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null } + val attachmentProto = SignalServiceProtos.AttachmentPointer.newBuilder() + attachmentProto.setId(attachment.serverID) + attachmentProto.setContentType(attachment.contentType) + attachmentProto.setSize(attachment.size) + attachmentProto.setFileName(attachment.fileName) + attachmentProto.setFlags(attachment.flags) + attachmentProto.setWidth(attachment.width) + attachmentProto.setHeight(attachment.height) + attachment.caption?.let { attachmentProto.setCaption(it) } + attachmentProto.setUrl(attachment.url) + attachmentProto.build() + } catch (e: Exception) { + Log.e("Loki","Failed to parse attachment as proto",e) + null + } + } + dataMessageProto.addAllAttachments(attachmentProtos) + // Link preview + val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview } + if (linkPreview != null) { + val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() + linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!) + linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!) + val attachmentProto = SignalServiceProtos.AttachmentPointer.newBuilder() + attachmentProto.setId(linkPreview.serverID) + attachmentProto.setContentType(linkPreview.contentType) + attachmentProto.setSize(linkPreview.size) + attachmentProto.setFileName(linkPreview.fileName) + attachmentProto.setFlags(linkPreview.flags) + attachmentProto.setWidth(linkPreview.width) + attachmentProto.setHeight(linkPreview.height) + linkPreview.caption?.let { attachmentProto.setCaption(it) } + attachmentProto.setUrl(linkPreview.url) + linkPreviewProto.setImage(attachmentProto.build()) + dataMessageProto.addPreview(linkPreviewProto.build()) + } + // Quote + val quote = message.quote + if (quote != null) { + val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder() + quoteProto.setId(quote.quotedMessageTimestamp) + quoteProto.setAuthor(quote.quoteePublicKey) + if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) } + dataMessageProto.setQuote(quoteProto.build()) + } + val messageServerID = message.serverID + // Profile + val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder() + profileProto.setDisplayName(senderDisplayName) + val profilePicture = message.profilePicture + if (profilePicture != null) { + profileProto.setProfilePicture(profilePicture.url) + dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey)) + } + dataMessageProto.setProfile(profileProto.build()) + /* TODO: the signal service proto needs to be synced with iOS + // Open group info + if (messageServerID != null) { + val openGroupProto = PublicChatInfo.newBuilder() + openGroupProto.setServerID(messageServerID) + dataMessageProto.setPublicChatInfo(openGroupProto.build()) + } + */ + // Signal group context + val groupProto = SignalServiceProtos.GroupContext.newBuilder() + groupProto.setId(ByteString.copyFrom(id)) + groupProto.setType(SignalServiceProtos.GroupContext.Type.DELIVER) + groupProto.setName(openGroup.displayName) + dataMessageProto.setGroup(groupProto.build()) + // Content + val content = SignalServiceProtos.Content.newBuilder() + content.setDataMessage(dataMessageProto.build()) + // Envelope + val builder = SignalServiceProtos.Envelope.newBuilder() + builder.type = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER + builder.source = senderPublicKey + builder.sourceDevice = 1 + builder.setContent(content.build().toByteString()) + builder.timestamp = message.timestamp + builder.serverTimestamp = message.serverTimestamp + val envelope = builder.build() + val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, messageServerID, openGroup.id) + Log.d("Loki", "Scheduling Job $job") + if (isBackgroundPoll) { + job.executeAsync().always { deferred.resolve(Unit) } + // The promise is just used to keep track of when we're done + } else { + JobQueue.shared.add(job) + } + } catch (e: Exception) { + Log.e("Loki", "Exception parsing message", e) + } + } + displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey + executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS) + isCaughtUp = true + isPollOngoing = false + deferred.resolve(Unit) + }.fail { + Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.") + isPollOngoing = false + } + return deferred.promise + } + + private fun pollForDisplayNames() { + if (displayNameUpdates.isEmpty()) { return } + val hexEncodedPublicKeys = displayNameUpdates + displayNameUpdates = setOf() + OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping -> + for (pair in mapping.entries) { + if (pair.key == userHexEncodedPublicKey) continue + val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})" + MessagingConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName) + } + }.fail { + displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys) + } + } + + private fun pollForDeletedMessages() { + OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs -> + val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getMessageID(it) } + deletedMessageIDs.forEach { + MessagingConfiguration.shared.messageDataProvider.deleteMessage(it) + } + }.fail { + Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.") + } + } + + private fun pollForModerators() { + OpenGroupAPI.getModerators(openGroup.channel, openGroup.server) + } + // endregion +} \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/opengroups/PublicChat.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/opengroups/PublicChat.kt index 1cf9ada3cc..75dea6b4ca 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/opengroups/PublicChat.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/api/opengroups/PublicChat.kt @@ -20,6 +20,7 @@ public data class PublicChat( @JvmStatic fun fromJSON(jsonAsString: String): PublicChat? { try { val json = JsonUtil.fromJson(jsonAsString) + if (!json.has("channel")) return null val channel = json.get("channel").asLong() val server = json.get("server").asText().toLowerCase() val displayName = json.get("displayName").asText() diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/database/LokiAPIDatabaseProtocol.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/database/LokiAPIDatabaseProtocol.kt index 0d24f218ba..2fdf5c9db8 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/database/LokiAPIDatabaseProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/database/LokiAPIDatabaseProtocol.kt @@ -24,6 +24,11 @@ interface LokiAPIDatabaseProtocol { fun getLastDeletionServerID(group: Long, server: String): Long? fun setLastDeletionServerID(group: Long, server: String, newValue: Long) fun setUserCount(group: Long, server: String, newValue: Int) + fun getLastMessageServerID(room: String, server: String): Long? + fun setLastMessageServerID(room: String, server: String, newValue: Long) + fun getLastDeletionServerID(room: String, server: String): Long? + fun setLastDeletionServerID(room: String, server: String, newValue: Long) + fun setUserCount(room: String, server: String, newValue: Int) fun getSessionRequestSentTimestamp(publicKey: String): Long? fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) fun getSessionRequestProcessedTimestamp(publicKey: String): Long?