From bddeafc46cf3877f05ee943b3cbdc81ae08e35ae Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 9 Apr 2021 13:43:47 +1000 Subject: [PATCH 01/39] feat: update the repo documentation readme and building file --- BUILDING.md | 15 +++------------ README.md | 6 ++++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 142069ff01..5fa550b3f9 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -21,15 +21,6 @@ Ensure that the following packages are installed from the Android SDK manager: In Android studio, this can be done from the Quickstart panel, choose "Configure" then "SDK Manager". In the SDK Tools tab of the SDK Manager, make sure that the "Android Support Repository" is installed, and that the latest "Android SDK build-tools" are installed. Click "OK" to return to the Quickstart panel. You may also need to install API version 28 in the SDK platforms tab. -You will then need to clone and run `./gradlew install` on each of the following repositories IN ORDER: - -* https://github.com/loki-project/loki-messenger-android-curve-25519 -* https://github.com/loki-project/loki-messenger-android-protocol -* https://github.com/loki-project/loki-messenger-android-meta -* https://github.com/loki-project/session-android-service - -This installs these dependencies into a local Maven repository which the main Session Android repository will then draw from. - Setting up a development environment and building from Android Studio ------------------------------------ @@ -37,7 +28,7 @@ Setting up a development environment and building from Android Studio 1. Open Android Studio. On a new installation, the Quickstart panel will appear. If you have open projects, close them using "File > Close Project" to see the Quickstart panel. 2. From the Quickstart panel, choose "Checkout from Version Control" then "git". -3. Paste the URL for the session-android project when prompted (https://github.com/loki-project/session-android.git). +3. Paste the URL for the session-android project when prompted (https://github.com/oxen-io/session-android.git). 4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes". 5. Default config options should be good enough. 6. Project initialization and building should proceed. @@ -49,7 +40,7 @@ The following steps should help you (re)build Session from the command line once 1. Checkout the session-android project source with the command: - git clone https://github.com/loki-project/session-android.git + git clone https://github.com/oxen-io/session-android.git 2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed. 3. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. For example: @@ -58,7 +49,7 @@ The following steps should help you (re)build Session from the command line once 4. Execute Gradle: - ./gradlew build + ./gradlew :app:build Contributing code ----------------- diff --git a/README.md b/README.md index e7a7279ea6..b4cd95a484 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,19 @@ [Download on the Google Play Store](https://getsession.org/android) +[Download via F-Droid](https://fdroid.getsession.org/fdroid/repo?fingerprint=DB0E5297EB65CC22D6BD93C869943BDCFCB6A07DC69A48A0DD8C7BA698EC04E6) + [Grab the APK here](https://github.com/loki-project/session-android/releases/latest) ## Summary -Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper). +Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper). ![AndroidSession](https://i.imgur.com/0YC9TyI.png) ## Want to contribute? Found a bug or have a feature request? -Please search for any [existing issues](https://github.com/loki-project/session-android/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing, try reading the Github issues page for ideas. +Please search for any [existing issues](https://github.com/oxen-io/session-android/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our `dev` branch. If you don't know where to start contributing, try reading the Github issues page for ideas. ## Build instructions From 57534d31e76a185b49621e66fbe76b24a9e3fbd2 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 9 Apr 2021 15:44:39 +1000 Subject: [PATCH 02/39] refactor: replace fdroid deep-link with the fdroid session landing page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4cd95a484..5e41159c45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [Download on the Google Play Store](https://getsession.org/android) -[Download via F-Droid](https://fdroid.getsession.org/fdroid/repo?fingerprint=DB0E5297EB65CC22D6BD93C869943BDCFCB6A07DC69A48A0DD8C7BA698EC04E6) +Add the [F-Droid repo](https://fdroid.getsession.org/) [Grab the APK here](https://github.com/loki-project/session-android/releases/latest) From 0eadc55325ec7d0d626a37311d5184bcd0feee97 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 13 Apr 2021 17:17:16 +1000 Subject: [PATCH 03/39] 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 | 40 +++- .../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, 485 insertions(+), 115 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.session.libsession.messaging.opengroups.OpenGroupV2 +import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.recipients.Recipient +import org.session.libsignal.service.loki.api.opengroups.PublicChat +import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol +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.* - -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.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? From 6f46bbefbeb0a90d3515bf179eaecb8a49f10968 Mon Sep 17 00:00:00 2001 From: Harris Date: Wed, 14 Apr 2021 23:25:38 +1000 Subject: [PATCH 04/39] feat: add more opengroupv2 functions and classes --- .../messaging/opengroups/OpenGroupAPIV2.kt | 15 +++--- .../opengroups/OpenGroupV2Message.kt | 52 +++++++++++++++++++ .../pollers/OpenGroupV2Poller.kt | 2 +- 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt 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 index c9bacb0d87..a0ee35bc9b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt @@ -1,10 +1,11 @@ package org.session.libsession.messaging.opengroups +import nl.komponents.kovenant.Promise import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error import org.session.libsession.messaging.utilities.DotNetAPI import java.util.* -class OpenGroupAPIV2: DotNetAPI() { +object OpenGroupAPIV2: DotNetAPI() { enum class Error { GENERIC, @@ -15,14 +16,14 @@ class OpenGroupAPIV2: DotNetAPI() { 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" + 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" + + fun getMessages(room: String, server: String): Promise, Exception> { + } - - } data class Info(val id: String, val name: String, val imageId: String?) diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt new file mode 100644 index 0000000000..a40bd53bf5 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt @@ -0,0 +1,52 @@ +package org.session.libsession.messaging.opengroups + +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.logging.Log +import org.whispersystems.curve25519.Curve25519 + +data class OpenGroupV2Message( + val serverID: Long?, + val sender: String?, + val sentTimestamp: Long, + // The serialized protobuf in base64 encoding + val base64EncodedData: String, + // When sending a message, the sender signs the serialized protobuf with their private key so that + // a receiving user can verify that the message wasn't tampered with. + val base64EncodedSignature: String? +) { + + companion object { + private val curve = Curve25519.getInstance(Curve25519.BEST) + } + + fun sign(): OpenGroupV2Message? { + if (base64EncodedData.isEmpty()) return null + val (publicKey, privateKey) = MessagingConfiguration.shared.storage.getUserKeyPair() ?: return null + + if (sender != publicKey) return null // only sign our own messages? + + val signature = try { + curve.calculateSignature(privateKey, Base64.decode(base64EncodedData)) + } catch (e: Exception) { + Log.e("Loki", "Couldn't sign OpenGroupV2Message", e) + return null + } + + return copy(base64EncodedSignature = Base64.encodeBytes(signature)) + } + + fun toJSON(): Map { + val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp) + serverID?.let { jsonMap["server_id"] = serverID } + sender?.let { jsonMap["public_key"] = sender } + base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } + } + + fun fromJSON(json: Map): OpenGroupV2Message? { + if (!json.containsKey("data") || !json.containsKey("timestamp")) return null + + } + + +} \ No newline at end of file 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 index 21e0c0d0cb..d1480670b9 100644 --- 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 @@ -76,7 +76,7 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor 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 senderDisplayName = MessagingConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.room, openGroup.server) ?: generateDisplayName(message.displayName) val id = openGroup.id.toByteArray() // Main message val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() From 96e604d06b8a59bf8987cdf916bda7d9145ba533 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 15 Apr 2021 17:17:55 +1000 Subject: [PATCH 05/39] feat: more opengroup in chat manager, poller and API. refactor mentions to libsession --- .../securesms/ApplicationContext.java | 2 +- .../conversation/ConversationActivity.java | 53 +++---- .../securesms/database/Storage.kt | 10 ++ .../securesms/loki/activities/HomeActivity.kt | 2 +- .../securesms/loki/api/PublicChatManager.kt | 38 ++++- .../loki/utilities/MentionManagerUtilities.kt | 4 +- .../views/MentionCandidateSelectionView.kt | 2 +- .../loki/views/MentionCandidateView.kt | 2 +- .../loki/views/ProfilePictureView.kt | 2 +- .../libsession/messaging/StorageProtocol.kt | 7 +- .../messaging/opengroups/OpenGroupAPIV2.kt | 149 ++++++++++++++++-- ...roupV2Message.kt => OpenGroupMessageV2.kt} | 20 ++- .../libsession}/utilities/mentions/Mention.kt | 2 +- .../utilities/mentions/MentionsManager.kt | 11 +- 14 files changed, 241 insertions(+), 63 deletions(-) rename libsession/src/main/java/org/session/libsession/messaging/opengroups/{OpenGroupV2Message.kt => OpenGroupMessageV2.kt} (69%) rename {libsignal/src/main/java/org/session/libsignal/service/loki => libsession/src/main/java/org/session/libsession}/utilities/mentions/Mention.kt (52%) rename {libsignal/src/main/java/org/session/libsignal/service/loki => libsession/src/main/java/org/session/libsession}/utilities/mentions/MentionsManager.kt (80%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 0ebcdc36a7..77f6614dd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -45,6 +45,7 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; import org.session.libsession.utilities.dynamiclanguage.LocaleParser; +import org.session.libsession.utilities.mentions.MentionsManager; import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.loki.api.PushNotificationAPI; @@ -52,7 +53,6 @@ import org.session.libsignal.service.loki.api.SnodeAPI; import org.session.libsignal.service.loki.api.SwarmAPI; import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol; -import org.session.libsignal.service.loki.utilities.mentions.MentionsManager; import org.session.libsignal.utilities.logging.Log; import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 3cceeb089c..8806cc23f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -83,21 +83,42 @@ import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; - - import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; +import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; +import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; +import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.DistributionTypes; +import org.session.libsession.messaging.threads.GroupRecord; +import org.session.libsession.messaging.threads.recipients.Recipient; +import org.session.libsession.messaging.threads.recipients.RecipientFormattingException; +import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; +import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.MediaTypes; +import org.session.libsession.utilities.ServiceUtil; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.ViewUtil; +import org.session.libsession.utilities.concurrent.AssertedSuccessListener; +import org.session.libsession.utilities.mentions.Mention; +import org.session.libsession.utilities.mentions.MentionsManager; +import org.session.libsession.utilities.views.Stub; import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.loki.api.opengroups.PublicChat; -import org.session.libsignal.service.loki.utilities.mentions.Mention; -import org.session.libsignal.service.loki.utilities.mentions.MentionsManager; import org.session.libsignal.service.loki.utilities.HexEncodingKt; import org.session.libsignal.service.loki.utilities.PublicKeyValidation; +import org.session.libsignal.utilities.concurrent.ListenableFuture; +import org.session.libsignal.utilities.concurrent.SettableFuture; +import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ExpirationDialog; import org.thoughtcrime.securesms.MediaOverviewActivity; @@ -121,7 +142,6 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; -import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase.Draft; @@ -135,7 +155,6 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; -import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker; @@ -158,9 +177,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; -import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; -import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; -import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteId; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -169,30 +185,11 @@ import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.threads.recipients.RecipientFormattingException; -import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.search.model.MessageResult; -import org.session.libsession.messaging.sending_receiving.MessageSender; -import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.PushCharacterCalculator; -import org.session.libsession.utilities.ServiceUtil; -import org.session.libsession.utilities.Util; - -import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; -import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; -import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; -import org.session.libsession.messaging.threads.GroupRecord; -import org.session.libsession.utilities.ExpirationUtil; -import org.session.libsession.utilities.views.Stub; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.concurrent.AssertedSuccessListener; -import org.session.libsignal.utilities.concurrent.ListenableFuture; -import org.session.libsignal.utilities.concurrent.SettableFuture; -import org.session.libsession.utilities.TextSecurePreferences; import java.io.IOException; import java.text.SimpleDateFormat; 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 62caac50f6..445fdb36b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -274,11 +274,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName) } + override fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) { + val groupID = "$server.$room" + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName) + } + override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? { val groupID = "$server.$channel" return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey) } + override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? { + val groupID = "$server.$room" + return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey) + } + override fun getLastMessageServerId(room: String, server: String): Long? { return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 8ed97bf3e2..3442c98c61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -33,7 +33,7 @@ import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.utilities.* -import org.session.libsignal.service.loki.utilities.mentions.MentionsManager +import org.session.libsession.utilities.mentions.MentionsManager import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.ApplicationContext 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 ef36d98de9..ab587c8171 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 @@ -6,10 +6,9 @@ import android.graphics.Bitmap import android.text.TextUtils import androidx.annotation.WorkerThread import org.session.libsession.messaging.MessagingConfiguration -import org.session.libsession.messaging.opengroups.OpenGroup -import org.session.libsession.messaging.opengroups.OpenGroupAPI -import org.session.libsession.messaging.opengroups.OpenGroupInfo +import org.session.libsession.messaging.opengroups.* import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller +import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.Util import org.session.libsignal.service.loki.api.opengroups.PublicChat @@ -21,7 +20,9 @@ import java.util.concurrent.Executors class PublicChatManager(private val context: Context) { private var chats = mutableMapOf() + private var v2Chats = mutableMapOf() private val pollers = mutableMapOf() + private val v2Pollers = mutableMapOf() private val observers = mutableMapOf() private var isPolling = false private val executorService = Executors.newScheduledThreadPool(4) @@ -53,6 +54,12 @@ class PublicChatManager(private val context: Context) { listenToThreadDeletion(threadId) if (!pollers.containsKey(threadId)) { pollers[threadId] = poller } } + for ((threadId, chat) in v2Chats) { + val poller = v2Pollers[threadId] ?: OpenGroupV2Poller(chat, executorService) + poller.startIfNeeded() + listenToThreadDeletion(threadId) + if (!v2Pollers.containsKey(threadId)) { v2Pollers[threadId] = poller } + } isPolling = true } @@ -73,6 +80,15 @@ class PublicChatManager(private val context: Context) { return addChat(server, channel, channelInfo) } + @WorkerThread + fun addChat(server: String, room: String): OpenGroupV2 { + // Ensure the auth token is acquired. + OpenGroupAPIV2.getAuthToken(server).get() + + val channelInfo = OpenGroupAPIV2.getChannelInfo(channel, server).get() + return addChat(server, room, channelInfo) + } + @WorkerThread public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup { val chat = PublicChat(channel, server, info.displayName, true) @@ -99,6 +115,20 @@ class PublicChatManager(private val context: Context) { return OpenGroup.from(chat) } + @WorkerThread + fun addChat(server: String, room: String, info: OpenGroupInfo): OpenGroupV2 { + val chat = OpenGroupV2(server, room, info.displayName, ) + var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) + var profilePicture: Bitmap? = null + if (threadID < 0) { + if (info.profilePictureURL.isNotEmpty()) { + val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(server, info.profilePictureURL) + profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray) + } + val result = GroupManager.createOpenGroup() + } + } + public fun removeChat(server: String, channel: Long) { val threadDB = DatabaseFactory.getThreadDatabase(context) val groupId = PublicChat.getId(channel, server) @@ -112,11 +142,13 @@ class PublicChatManager(private val context: Context) { private fun refreshChatsAndPollers() { val storage = MessagingConfiguration.shared.storage val chatsInDB = storage.getAllOpenGroups() + val v2ChatsInDB = storage.getAllV2OpenGroups() val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) } removedChatThreadIds.forEach { pollers.remove(it)?.stop() } // Only append to chats if we have a thread for the chat chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap() + v2Chats = v2ChatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap() } private fun listenToThreadDeletion(threadID: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt index f59bc7b71a..d00f56851a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionManagerUtilities.kt @@ -1,10 +1,10 @@ package org.thoughtcrime.securesms.loki.utilities import android.content.Context +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.mentions.MentionsManager import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MessageRecord -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.service.loki.utilities.mentions.MentionsManager object MentionManagerUtilities { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt index ddda015d77..63445c4ee4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt @@ -7,10 +7,10 @@ import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ListView +import org.session.libsession.utilities.mentions.Mention import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.mms.GlideRequests -import org.session.libsignal.service.loki.utilities.mentions.Mention class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { private var mentionCandidates = listOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt index f85e5e9867..9c69333904 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt @@ -9,7 +9,7 @@ import android.widget.LinearLayout import kotlinx.android.synthetic.main.view_mention_candidate.view.* import network.loki.messenger.R import org.session.libsession.messaging.opengroups.OpenGroupAPI -import org.session.libsignal.service.loki.utilities.mentions.Mention +import org.session.libsession.utilities.mentions.Mention import org.thoughtcrime.securesms.mms.GlideRequests class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index a092841633..c6c69d158b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -14,7 +14,7 @@ import org.session.libsession.messaging.avatars.ProfileContactPhoto 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.utilities.mentions.MentionsManager +import org.session.libsession.utilities.mentions.MentionsManager import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.mms.GlideRequests 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 95e5eb680a..66def868f2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -71,8 +71,8 @@ interface StorageProtocol { fun setOpenGroupPublicKey(server: String, newValue: String) // Open Group User Info - fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) - fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? + fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) + fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? // Open Group Metadata @@ -180,4 +180,7 @@ interface StorageProtocol { fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) fun getOpenGroupProfilePictureURL(group: Long, server: String): String? + fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) + fun getOpenGroupDisplayName(publicKey: String, channel: 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 index a0ee35bc9b..b0fde9913f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt @@ -1,32 +1,153 @@ package org.session.libsession.messaging.opengroups import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import okhttp3.HttpUrl +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error -import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsignal.service.loki.api.utilities.HTTP import java.util.* -object OpenGroupAPIV2: DotNetAPI() { - - enum class Error { - GENERIC, - PARSING_FAILED, - DECRYPTION_FAILED, - SIGNING_FAILED, - INVALID_URL, - NO_PUBLIC_KEY - } +object OpenGroupAPIV2 { 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" - fun getMessages(room: String, server: String): Promise, Exception> { + sealed class Error : Exception() { + object GENERIC : Error() + object PARSING_FAILED : Error() + object DECRYPTION_FAILED : Error() + object SIGNING_FAILED : Error() + object INVALID_URL : Error() + object NO_PUBLIC_KEY : Error() + } + + data class Info( + val id: String, + val name: String, + val imageID: String + ) + + data class Request( + val verb: HTTP.Verb, + val room: String?, + val server: String, + val endpoint: String, + val queryParameters: Map, + val parameters: Any, + val headers: Map, + val isAuthRequired: Boolean, + // Always `true` under normal circumstances. You might want to disable + // this when running over Lokinet. + val useOnionRouting: Boolean + ) + + private fun send(request: Request): Promise { + val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL) + val urlBuilder = HttpUrl.Builder() + .scheme(parsed.scheme()) + .host(parsed.host()) + .addPathSegment(request.endpoint) + + for ((key, value) in request.queryParameters) { + urlBuilder.addQueryParameter(key, value) + } + + fun execute(token: String?): Promise, Exception> { + } + return if (request.isAuthRequired) { + getAuthToken(request.room!!, request.server).bind(::execute) + } else { + execute(null) + } + } + + fun getAuthToken(room: String, server: String): Promise { + val storage = MessagingConfiguration.shared.storage + return storage.getAuthToken(room, server)?.let { + Promise.of(it) + } ?: run { + requestNewAuthToken(room, server) + .bind { claimAuthToken(it, room, server) } + .success { authToken -> + storage.setAuthToken(room, server, authToken) + } + } + } + + fun requestNewAuthToken(room: String, server: String): Promise { + val (publicKey, _) = MessagingConfiguration.shared.storage.getUserKeyPair() + ?: return Promise.ofFail(Error.GENERIC) + val queryParameters = mutableMapOf("public_key" to publicKey) } -} + fun claimAuthToken(authToken: String, room: String, server: String): Promise { + TODO("implement") + } -data class Info(val id: String, val name: String, val imageId: String?) + fun deleteAuthToken(room: String, server: String): Promise { + TODO("implement") + } + + fun upload(file: ByteArray, room: String, server: String): Promise { + TODO("implement") + } + + fun download(file: Long, room: String, server: String): Promise { + TODO("implement") + } + + fun send(message: OpenGroupMessageV2, room: String, server: String): Promise { + TODO("implement") + } + + fun getMessages(room: String, server: String): Promise, Exception> { + TODO("implement") + } + + fun deleteMessage(serverID: Long, room: String, server: String): Promise { + TODO("implement") + } + + fun getDeletedMessages(room: String, server: String): Promise, Exception> { + TODO("implement") + } + + fun getModerators(room: String, server: String): Promise, Exception> { + TODO("implement") + } + + fun ban(publicKey: String, room: String, server: String): Promise { + TODO("implement") + } + + fun unban(publicKey: String, room: String, server: String): Promise { + TODO("implement") + } + + fun isUserModerator(publicKey: String, room: String, server: String): Promise { + TODO("implement") + } + + fun getDefaultRoomsIfNeeded() { + TODO("implement") + } + + fun getInfo(room: String, server: String): Promise { + TODO("implement") + } + + fun getAllRooms(server: String): Promise, Exception> { + TODO("implement") + } + + fun getMemberCount(room: String, server: String): Promise { + TODO("implement") + } + +} fun Error.errorDescription() = when (this) { Error.GENERIC -> "An error occurred." diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt similarity index 69% rename from libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt rename to libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt index a40bd53bf5..80d72228d3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupV2Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt @@ -5,7 +5,7 @@ import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.logging.Log import org.whispersystems.curve25519.Curve25519 -data class OpenGroupV2Message( +data class OpenGroupMessageV2( val serverID: Long?, val sender: String?, val sentTimestamp: Long, @@ -20,7 +20,7 @@ data class OpenGroupV2Message( private val curve = Curve25519.getInstance(Curve25519.BEST) } - fun sign(): OpenGroupV2Message? { + fun sign(): OpenGroupMessageV2? { if (base64EncodedData.isEmpty()) return null val (publicKey, privateKey) = MessagingConfiguration.shared.storage.getUserKeyPair() ?: return null @@ -41,11 +41,21 @@ data class OpenGroupV2Message( serverID?.let { jsonMap["server_id"] = serverID } sender?.let { jsonMap["public_key"] = sender } base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } + return jsonMap } - fun fromJSON(json: Map): OpenGroupV2Message? { - if (!json.containsKey("data") || !json.containsKey("timestamp")) return null - + fun fromJSON(json: Map): OpenGroupMessageV2? { + val base64EncodedData = json["data"] as? String ?: return null + val sentTimestamp = json["timestamp"] as? Long ?: return null + val serverID = json["server_id"] as? Long + val sender = json["public_key"] as? String + val base64EncodedSignature = json["signature"] as? String + return OpenGroupMessageV2(serverID = serverID, + sender = sender, + sentTimestamp = sentTimestamp, + base64EncodedData = base64EncodedData, + base64EncodedSignature = base64EncodedSignature + ) } diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/Mention.kt b/libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt similarity index 52% rename from libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/Mention.kt rename to libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt index 8d97c45268..7900439d57 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/Mention.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt @@ -1,3 +1,3 @@ -package org.session.libsignal.service.loki.utilities.mentions +package org.session.libsession.utilities.mentions data class Mention(val publicKey: String, val displayName: String) diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/MentionsManager.kt b/libsession/src/main/java/org/session/libsession/utilities/mentions/MentionsManager.kt similarity index 80% rename from libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/MentionsManager.kt rename to libsession/src/main/java/org/session/libsession/utilities/mentions/MentionsManager.kt index 2edf6f677c..e784ff49a7 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/MentionsManager.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/mentions/MentionsManager.kt @@ -1,5 +1,6 @@ -package org.session.libsignal.service.loki.utilities.mentions +package org.session.libsession.utilities.mentions +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol @@ -9,9 +10,9 @@ class MentionsManager(private val userPublicKey: String, private val threadDatab companion object { - public lateinit var shared: MentionsManager + lateinit var shared: MentionsManager - public fun configureIfNeeded(userPublicKey: String, threadDatabase: LokiThreadDatabaseProtocol, userDatabase: LokiUserDatabaseProtocol) { + fun configureIfNeeded(userPublicKey: String, threadDatabase: LokiThreadDatabaseProtocol, userDatabase: LokiUserDatabaseProtocol) { if (::shared.isInitialized) { return; } shared = MentionsManager(userPublicKey, threadDatabase, userDatabase) } @@ -30,11 +31,15 @@ class MentionsManager(private val userPublicKey: String, private val threadDatab // Prepare val cache = userPublicKeyCache[threadID] ?: return listOf() // Gather candidates + val storage = MessagingConfiguration.shared.storage val publicChat = threadDatabase.getPublicChat(threadID) + val openGroupV2 = storage.getV2OpenGroup(threadID.toString()) var candidates: List = cache.mapNotNull { publicKey -> val displayName: String? if (publicChat != null) { displayName = userDatabase.getServerDisplayName(publicChat.id, publicKey) + } else if (openGroupV2 != null) { + displayName = userDatabase.getServerDisplayName(openGroupV2.id, publicKey) } else { displayName = userDatabase.getDisplayName(publicKey) } From aea23a6fc1f06a0a5ff07b690d6cef03a8eb1c9d Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 19 Apr 2021 10:16:38 +1000 Subject: [PATCH 06/39] feat: finishing up OpenGroupAPIV2.kt calls --- .../securesms/database/Storage.kt | 10 +- .../loki/database/LokiAPIDatabase.kt | 10 +- .../libsession/messaging/StorageProtocol.kt | 1 + .../messaging/opengroups/OpenGroupAPIV2.kt | 253 +++++++++++++++--- .../opengroups/OpenGroupMessageV2.kt | 31 ++- .../session/libsession/utilities/AESGCM.kt | 19 +- .../loki/database/LokiAPIDatabaseProtocol.kt | 2 +- 7 files changed, 264 insertions(+), 62 deletions(-) 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 445fdb36b6..6e4a26cdef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -314,15 +314,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun getLastDeletionServerId(room: String, server: String): Long? { - TODO("Not yet implemented") + return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server) } override fun setLastDeletionServerId(room: String, server: String, newValue: Long) { - TODO("Not yet implemented") + DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue) } override fun removeLastDeletionServerId(room: String, server: String) { - TODO("Not yet implemented") + DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server) + } + + override fun setUserCount(room: String, server: String, newValue: Long) { + DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue) } override fun getLastDeletionServerID(group: Long, server: String): Long? { 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 e4ea2d0098..beeaaa8a1f 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 @@ -316,7 +316,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( fun removeLastMessageServerID(room: String, server:String) { val database = databaseHelper.writableDatabase - val index = "$server.$channel" + val index = "$server.$room" database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) } @@ -350,6 +350,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } + fun removeLastDeletionServerID(room: String, server: String) { + val database = databaseHelper.writableDatabase + val index = "$server.$room" + database.delete(lastDeletionServerIDTable, "$lastDeletionServerID = ?", wrap(index)) + } + fun removeLastDeletionServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" @@ -379,7 +385,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) } - override fun setUserCount(room: String, server: String, newValue: Int) { + override fun setUserCount(room: String, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$room" val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() )) 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 66def868f2..c15e62ad3e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -78,6 +78,7 @@ interface StorageProtocol { fun updateTitle(groupID: String, newValue: String) fun updateProfilePicture(groupID: String, newValue: ByteArray) + fun setUserCount(room: String, server: String, newValue: Long) // Last Message Server ID fun getLastMessageServerId(room: String, server: String): Long? 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 index b0fde9913f..51a354af83 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPIV2.kt @@ -1,19 +1,36 @@ package org.session.libsession.messaging.opengroups +import nl.komponents.kovenant.Kovenant import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.RequestBody import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.utilities.AESGCM import org.session.libsignal.service.loki.api.utilities.HTTP +import org.session.libsignal.service.loki.api.utilities.HTTP.Verb.* +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded +import org.session.libsignal.service.loki.utilities.toHexString +import org.session.libsignal.utilities.Base64.* +import org.session.libsignal.utilities.JsonUtil +import org.session.libsignal.utilities.createContext +import org.session.libsignal.utilities.logging.Log +import org.whispersystems.curve25519.Curve25519 import java.util.* object OpenGroupAPIV2 { - private val moderators: HashMap>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) + 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" + private val sharedContext = Kovenant.createContext() + private val curve = Curve25519.getInstance(Curve25519.BEST) + sealed class Error : Exception() { object GENERIC : Error() object PARSING_FAILED : Error() @@ -26,7 +43,7 @@ object OpenGroupAPIV2 { data class Info( val id: String, val name: String, - val imageID: String + val imageID: String? ) data class Request( @@ -34,30 +51,67 @@ object OpenGroupAPIV2 { val room: String?, val server: String, val endpoint: String, - val queryParameters: Map, - val parameters: Any, - val headers: Map, - val isAuthRequired: Boolean, + val queryParameters: Map = mapOf(), + val parameters: Any? = null, + val headers: Map = mapOf(), + val isAuthRequired: Boolean = true, // Always `true` under normal circumstances. You might want to disable // this when running over Lokinet. - val useOnionRouting: Boolean + val useOnionRouting: Boolean = true ) - private fun send(request: Request): Promise { + private fun createBody(parameters: Any): RequestBody { + val parametersAsJSON = JsonUtil.toJson(parameters) + return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) + } + + private fun send(request: Request): Promise, Exception> { val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL) val urlBuilder = HttpUrl.Builder() .scheme(parsed.scheme()) .host(parsed.host()) .addPathSegment(request.endpoint) - for ((key, value) in request.queryParameters) { - urlBuilder.addQueryParameter(key, value) + if (request.verb == GET) { + for ((key, value) in request.queryParameters) { + urlBuilder.addQueryParameter(key, value) + } } fun execute(token: String?): Promise, Exception> { + val requestBuilder = okhttp3.Request.Builder() + .url(urlBuilder.build()) + if (request.isAuthRequired) { + if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request") + requestBuilder.addHeader("Authorization", token) + } + when (request.verb) { + GET -> requestBuilder.get() + PUT -> requestBuilder.put(createBody(request.parameters!!)) + POST -> requestBuilder.post(createBody(request.parameters!!)) + DELETE -> requestBuilder.delete(createBody(request.parameters!!)) + } + + if (!request.room.isNullOrEmpty()) { + requestBuilder.header("Room", request.room) + } + + if (request.useOnionRouting) { + val publicKey = MessagingConfiguration.shared.storage.getOpenGroupPublicKey(request.server) + ?: return Promise.ofFail(Error.NO_PUBLIC_KEY) + return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey) + .fail { e -> + if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException + && e.statusCode == 401) { + MessagingConfiguration.shared.storage.removeAuthToken(request.server) + } + } + } else { + return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) + } } return if (request.isAuthRequired) { - getAuthToken(request.room!!, request.server).bind(::execute) + getAuthToken(request.room!!, request.server).bind(sharedContext) { execute(it) } } else { execute(null) } @@ -69,7 +123,7 @@ object OpenGroupAPIV2 { Promise.of(it) } ?: run { requestNewAuthToken(room, server) - .bind { claimAuthToken(it, room, server) } + .bind(sharedContext) { claimAuthToken(it, room, server) } .success { authToken -> storage.setAuthToken(room, server, authToken) } @@ -77,75 +131,204 @@ object OpenGroupAPIV2 { } fun requestNewAuthToken(room: String, server: String): Promise { - val (publicKey, _) = MessagingConfiguration.shared.storage.getUserKeyPair() + val (publicKey, privateKey) = MessagingConfiguration.shared.storage.getUserKeyPair() ?: return Promise.ofFail(Error.GENERIC) val queryParameters = mutableMapOf("public_key" to publicKey) - + val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null) + return send(request).map(sharedContext) { json -> + val challenge = json["challenge"] as? Map<*,*> ?: throw Error.PARSING_FAILED + val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.PARSING_FAILED + val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.PARSING_FAILED + val ciphertext = decode(base64EncodedCiphertext) + val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey) + val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey) + val tokenAsData = try { + AESGCM.decrypt(ciphertext, symmetricKey) + } catch (e: Exception) { + throw Error.DECRYPTION_FAILED + } + tokenAsData.toHexString() + } } fun claimAuthToken(authToken: String, room: String, server: String): Promise { - TODO("implement") + val parameters = mapOf("public_key" to MessagingConfiguration.shared.storage.getUserPublicKey()!!) + val headers = mapOf("Authorization" to authToken) + val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token", + parameters = parameters, headers = headers, isAuthRequired = false) + return send(request).map(sharedContext) { authToken } } - fun deleteAuthToken(room: String, server: String): Promise { - TODO("implement") + fun deleteAuthToken(room: String, server: String): Promise { + val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token") + return send(request).map(sharedContext) { + MessagingConfiguration.shared.storage.removeAuthToken(room, server) + } } + // region Sending fun upload(file: ByteArray, room: String, server: String): Promise { - TODO("implement") + val base64EncodedFile = encodeBytes(file) + val parameters = mapOf("file" to base64EncodedFile) + val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters) + return send(request).map(sharedContext) { json -> + json["result"] as? Long ?: throw Error.PARSING_FAILED + } } fun download(file: Long, room: String, server: String): Promise { - TODO("implement") + val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file") + return send(request).map(sharedContext) { json -> + val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED + decode(base64EncodedFile) ?: throw Error.PARSING_FAILED + } } fun send(message: OpenGroupMessageV2, room: String, server: String): Promise { - TODO("implement") + val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED) + val json = signedMessage.toJSON() + val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = json) + return send(request).map(sharedContext) { + @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map ?: throw Error.PARSING_FAILED + OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED + } } + // endregion + // region Messages fun getMessages(room: String, server: String): Promise, Exception> { - TODO("implement") - } + val storage = MessagingConfiguration.shared.storage + val queryParameters = mutableMapOf() + storage.getLastMessageServerId(room,server)?.let { lastId -> + queryParameters += "from_server_id" to lastId.toString() + } + val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters) + return send(request).map(sharedContext) { jsonList -> + @Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List> ?: throw Error.PARSING_FAILED + val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 + var currentMax = lastMessageServerId + val messages = rawMessages.mapNotNull { json -> + val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null + if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null + val sender = message.sender + val data = decode(message.base64EncodedData) + val signature = decode(message.base64EncodedSignature) + val publicKey = sender.removing05PrefixIfNeeded().encodeToByteArray() + val isValid = curve.verifySignature(publicKey, data, signature) + if (!isValid) { + Log.d("Loki", "Ignoring message with invalid signature") + return@mapNotNull null + } + if (message.serverID > lastMessageServerId) { + currentMax = message.serverID + } + message + } + storage.setLastMessageServerId(room,server,currentMax) + messages + } + } + // endregion + + // region Message Deletion fun deleteMessage(serverID: Long, room: String, server: String): Promise { - TODO("implement") + val request = Request(verb = DELETE, room = room, server = server, endpoint = "message/$serverID") + return send(request).map(sharedContext) { + Log.d("Loki", "Deleted server message") + } } fun getDeletedMessages(room: String, server: String): Promise, Exception> { - TODO("implement") + val storage = MessagingConfiguration.shared.storage + val queryParameters = mutableMapOf() + storage.getLastDeletionServerId(room, server)?.let { last -> + queryParameters["from_server_id"] = last.toString() + } + val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters) + return send(request).map(sharedContext) { json -> + @Suppress("UNCHECKED_CAST") val serverIDs = json["ids"] as? List ?: throw Error.PARSING_FAILED + val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 + val serverID = serverIDs.maxOrNull() ?: 0 + if (serverID > lastMessageServerId) { + storage.setLastDeletionServerId(room, server, serverID) + } + serverIDs + } } + // endregion + // region Moderation fun getModerators(room: String, server: String): Promise, Exception> { - TODO("implement") + val request = Request(verb = GET, room = room, server = server, endpoint = "moderators") + return send(request).map(sharedContext) { json -> + @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List ?: throw Error.PARSING_FAILED + val id = "$server.$room" + moderators[id] = moderatorsJson.toMutableSet() + moderatorsJson + } } fun ban(publicKey: String, room: String, server: String): Promise { - TODO("implement") + val parameters = mapOf("public_key" to publicKey) + val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters) + return send(request).map(sharedContext) { + Log.d("Loki", "Banned user $publicKey from $server.$room") + } } fun unban(publicKey: String, room: String, server: String): Promise { - TODO("implement") + val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey") + return send(request).map(sharedContext) { + Log.d("Loki", "Unbanned user $publicKey from $server.$room") + } } - fun isUserModerator(publicKey: String, room: String, server: String): Promise { - TODO("implement") - } + fun isUserModerator(publicKey: String, room: String, server: String): Boolean = moderators["$server.$room"]?.contains(publicKey) ?: false + // endregion - fun getDefaultRoomsIfNeeded() { - TODO("implement") + // region General + fun getDefaultRoomsIfNeeded(): Promise, Exception> { + val storage = MessagingConfiguration.shared.storage + storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY) + return getAllRooms(DEFAULT_SERVER) } fun getInfo(room: String, server: String): Promise { - TODO("implement") + val request = Request(verb = GET, room = room, server = server, endpoint = "rooms/$room", isAuthRequired = false) + return send(request).map(sharedContext) { json -> + val rawRoom = json["room"] as? Map<*,*> ?: throw Error.PARSING_FAILED + val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED + val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED + val imageID = rawRoom["image_id"] as? String + Info(id = id, name = name, imageID = imageID) + } } fun getAllRooms(server: String): Promise, Exception> { - TODO("implement") + val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false) + return send(request).map(sharedContext) { json -> + val rawRooms = json["rooms"] as? Map<*,*> ?: throw Error.PARSING_FAILED + rawRooms.mapNotNull { + val roomJson = it as? Map<*, *> ?: return@mapNotNull null + val id = roomJson["id"] as? String ?: return@mapNotNull null + val name = roomJson["name"] as? String ?: return@mapNotNull null + val imageId = roomJson["image_id"] as? String + Info(id, name, imageId) + } + } } fun getMemberCount(room: String, server: String): Promise { - TODO("implement") + val request = Request(verb = GET, room = room, server = server, endpoint = "member_count") + return send(request).map(sharedContext) { json -> + val memberCount = json["member_count"] as? Long ?: throw Error.PARSING_FAILED + val storage = MessagingConfiguration.shared.storage + storage.setUserCount(room, server, memberCount) + memberCount + } } + // endregion } diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt index 80d72228d3..fa3195a90f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessageV2.kt @@ -18,6 +18,21 @@ data class OpenGroupMessageV2( companion object { private val curve = Curve25519.getInstance(Curve25519.BEST) + + fun fromJSON(json: Map): OpenGroupMessageV2? { + val base64EncodedData = json["data"] as? String ?: return null + val sentTimestamp = json["timestamp"] as? Long ?: return null + val serverID = json["server_id"] as? Long + val sender = json["public_key"] as? String + val base64EncodedSignature = json["signature"] as? String + return OpenGroupMessageV2(serverID = serverID, + sender = sender, + sentTimestamp = sentTimestamp, + base64EncodedData = base64EncodedData, + base64EncodedSignature = base64EncodedSignature + ) + } + } fun sign(): OpenGroupMessageV2? { @@ -43,20 +58,4 @@ data class OpenGroupMessageV2( base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } return jsonMap } - - fun fromJSON(json: Map): OpenGroupMessageV2? { - val base64EncodedData = json["data"] as? String ?: return null - val sentTimestamp = json["timestamp"] as? Long ?: return null - val serverID = json["server_id"] as? Long - val sender = json["public_key"] as? String - val base64EncodedSignature = json["signature"] as? String - return OpenGroupMessageV2(serverID = serverID, - sender = sender, - sentTimestamp = sentTimestamp, - base64EncodedData = base64EncodedData, - base64EncodedSignature = base64EncodedSignature - ) - } - - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt index fcbbf548d8..f5a170d8bd 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt @@ -1,14 +1,16 @@ package org.session.libsession.utilities -import org.whispersystems.curve25519.Curve25519 +import androidx.annotation.WorkerThread import org.session.libsignal.libsignal.util.ByteUtil import org.session.libsignal.service.internal.util.Util import org.session.libsignal.utilities.Hex +import org.whispersystems.curve25519.Curve25519 import javax.crypto.Cipher import javax.crypto.Mac import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec +@WorkerThread internal object AESGCM { internal data class EncryptionResult( @@ -31,6 +33,16 @@ internal object AESGCM { return cipher.doFinal(ciphertext) } + /** + * Sync. Don't call from the main thread. + */ + internal fun generateSymmetricKey(x25519PublicKey: ByteArray, x25519PrivateKey: ByteArray): ByteArray { + val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, x25519PrivateKey) + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256")) + return mac.doFinal(ephemeralSharedSecret) + } + /** * Sync. Don't call from the main thread. */ @@ -47,10 +59,7 @@ internal object AESGCM { internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult { val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey) val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair() - val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey) - val mac = Mac.getInstance("HmacSHA256") - mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256")) - val symmetricKey = mac.doFinal(ephemeralSharedSecret) + val symmetricKey = generateSymmetricKey(x25519PublicKey, ephemeralKeyPair.privateKey) val ciphertext = encrypt(plaintext, symmetricKey) return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey) } 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 2fdf5c9db8..8107c1844b 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 @@ -28,7 +28,7 @@ interface LokiAPIDatabaseProtocol { 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 setUserCount(room: String, server: String, newValue: Long) fun getSessionRequestSentTimestamp(publicKey: String): Long? fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) fun getSessionRequestProcessedTimestamp(publicKey: String): Long? From 1e40c861d17a3d2bbe9d8b8ac92137ce1fc17f27 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 20 Apr 2021 17:02:14 +1000 Subject: [PATCH 07/39] make screenlock work within 60s --- .../securesms/preferences/AppProtectionPreferenceFragment.java | 3 ++- .../org/thoughtcrime/securesms/service/KeyCachingService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index db9484f9b0..e516916de4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -102,7 +102,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment if (duration == 0) { TextSecurePreferences.setScreenLockTimeout(getContext(), 0); } else { - long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60); + long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration); +// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60); TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index e1c2350ef1..cde706c6e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -210,7 +210,7 @@ public class KeyCachingService extends Service { boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context); long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context); - boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context); + boolean screenLockActive = screenTimeout >= 0 && TextSecurePreferences.isScreenLockEnabled(context); if (!appVisible && secretSet && (passLockActive || screenLockActive)) { long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context); From 1e164f8648b8b436d1e7885c2ce78b5abc42b975 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 20 Apr 2021 17:22:36 +1000 Subject: [PATCH 08/39] feat: adding default group handling to frontend viewmodel --- .../loki/activities/JoinPublicChatActivity.kt | 28 +++- .../securesms/loki/api/PublicChatManager.kt | 20 +-- .../loki/viewmodel/DefaultGroupsViewModel.kt | 39 ++++++ .../securesms/loki/viewmodel/State.kt | 7 + .../fragment_enter_chat_url.xml | 43 +++++- .../res/layout/fragment_enter_chat_url.xml | 12 +- .../xml/network_security_configuration.xml | 11 +- .../messaging/opengroups/OpenGroupAPIV2.kt | 67 ++++++--- .../opengroups/OpenGroupMessageV2.kt | 7 + .../sending_receiving/MessageSender.kt | 4 +- .../pollers/OpenGroupV2Poller.kt | 130 ++---------------- .../libsession/snode/OnionRequestAPI.kt | 14 +- .../org/session/libsession/snode/SnodeAPI.kt | 23 ++-- .../session/libsession/snode/SnodeMessage.kt | 10 +- .../libsignal/service/loki/api/SwarmAPI.kt | 16 ++- 15 files changed, 246 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index 79c107c3ca..51e6816d73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -9,8 +9,7 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.* import androidx.lifecycle.lifecycleScope import kotlinx.android.synthetic.main.activity_join_public_chat.* import kotlinx.android.synthetic.main.fragment_enter_chat_url.* @@ -18,13 +17,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R +import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities +import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroup +import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel +import org.thoughtcrime.securesms.loki.viewmodel.State class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private val adapter = JoinPublicChatActivityAdapter(this) @@ -122,14 +124,34 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity // region Enter Chat URL Fragment class EnterChatURLFragment : Fragment() { + // factory producer is app scoped because default groups will want to stick around for app lifetime + private val viewModel by activityViewModels() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) } + private fun populateDefaultGroups(groups: List) { + Log.d("Loki", "Got some default groups $groups") + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } + viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> + when (state) { + State.Loading -> { + // show a loader here probs + } + is State.Error -> { + // hide the loader and the + } + is State.Success -> { + populateDefaultGroups(state.value) + } + } + } } private fun joinPublicChatIfPossible() { 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 ab587c8171..de45a92075 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 @@ -30,7 +30,7 @@ class PublicChatManager(private val context: Context) { public fun areAllCaughtUp(): Boolean { var areAllCaughtUp = true refreshChatsAndPollers() - for ((threadID, chat) in chats) { + for ((threadID, _) in chats) { val poller = pollers[threadID] areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true } @@ -83,9 +83,9 @@ class PublicChatManager(private val context: Context) { @WorkerThread fun addChat(server: String, room: String): OpenGroupV2 { // Ensure the auth token is acquired. - OpenGroupAPIV2.getAuthToken(server).get() + OpenGroupAPIV2.getAuthToken(room, server).get() - val channelInfo = OpenGroupAPIV2.getChannelInfo(channel, server).get() + val channelInfo = OpenGroupAPIV2.getInfo(room, server).get() return addChat(server, room, channelInfo) } @@ -116,17 +116,19 @@ class PublicChatManager(private val context: Context) { } @WorkerThread - fun addChat(server: String, room: String, info: OpenGroupInfo): OpenGroupV2 { - val chat = OpenGroupV2(server, room, info.displayName, ) - var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) + fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info): OpenGroupV2 { + val chat = OpenGroupV2(server, room, info.id, info.name, info.imageID) + val threadID = GroupManager.getOpenGroupThreadID(chat.id, context) var profilePicture: Bitmap? = null if (threadID < 0) { - if (info.profilePictureURL.isNotEmpty()) { - val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(server, info.profilePictureURL) + val imageID = info.imageID + if (!imageID.isNullOrEmpty()) { + val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(imageID) profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray) } - val result = GroupManager.createOpenGroup() + val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name) } + return chat } public fun removeChat(server: String, channel: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt new file mode 100644 index 0000000000..5bb564e72f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.loki.viewmodel + +import androidx.lifecycle.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import org.session.libsession.messaging.opengroups.OpenGroupAPIV2 +import org.session.libsignal.utilities.logging.Log + +class DefaultGroupsViewModel : ViewModel() { + + init { + OpenGroupAPIV2.getDefaultRoomsIfNeeded() + } + + val defaultRooms = OpenGroupAPIV2.defaultRooms.asLiveData().distinctUntilChanged().switchMap { groups -> + liveData { + // load images etc + emit(State.Loading) + val images = groups.filterNot { it.imageID.isNullOrEmpty() }.map { group -> + val image = viewModelScope.async(Dispatchers.IO) { + try { + OpenGroupAPIV2.downloadOpenGroupProfilePicture(group.imageID!!) + } catch (e: Exception) { + Log.e("Loki", "Error getting group profile picture", e) + null + } + } + group.id to image + }.toMap() + val defaultGroups = groups.map { group -> + DefaultGroup(group.id, group.name, images[group.id]?.await()) + } + emit(State.Success(defaultGroups)) + } + } + +} + +data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt new file mode 100644 index 0000000000..88223b9099 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.loki.viewmodel + +sealed class State { + object Loading : State() + data class Success(val value: T): State() + data class Error(val error: Exception): State() +} diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index a1bf13aedc..b56e3e110d 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -1,6 +1,6 @@ - + android:hint="@string/fragment_enter_chat_url_edit_text_hint" /> + + + + + +