From 0eadc55325ec7d0d626a37311d5184bcd0feee97 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 13 Apr 2021 17:17:16 +1000 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 1e164f8648b8b436d1e7885c2ce78b5abc42b975 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 20 Apr 2021 17:22:36 +1000 Subject: [PATCH 05/26] 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" /> + + + + + +