diff --git a/Makefile b/Makefile index cefa712cda7524012c97004b48ddb1e62816fef6..2a216e1b47600ba21525b784257378a8271ba502 100644 --- a/Makefile +++ b/Makefile @@ -382,6 +382,14 @@ sc.db.rollback_n: ## run migrations docker-compose -f docker-compose-sc.yml \ run -e SIGNALC_ENV=test --entrypoint 'gradle --console=plain rollbackCount -PliquibaseCommandValue=$(N)' signalc +sc.db.clear_checksums: ## run migrations + echo "----- rolling back 1 development migration" && \ + docker-compose -f docker-compose-sc.yml \ + run -e SIGNALC_ENV=development --entrypoint 'gradle --console=plain clearCheckSums' signalc && \ + echo "----- rolling back 1 test migration" && \ + docker-compose -f docker-compose-sc.yml \ + run -e SIGNALC_ENV=test --entrypoint 'gradle --console=plain clearCheckSums' signalc + sc.db.psql: # get a psql shell on signalc db ./bin/sc/psql diff --git a/signalc/build.gradle.kts b/signalc/build.gradle.kts index 36deb76a02301d718e1cfde14e977d3fcc8626fc..0aa0f4af0407a9c005175b5fc7b0d5cb6bbb48a0 100644 --- a/signalc/build.gradle.kts +++ b/signalc/build.gradle.kts @@ -78,7 +78,7 @@ object Versions { const val kotlinSerialization = "1.2.1" const val h2 = "1.4.199" const val hikariCp = "4.0.3" - const val libsignal = "2.15.3_unofficial_t23_m1" + const val libsignal = "2.15.3_unofficial_t23_m2_WIP" const val liquibase = "4.2.2" const val liquibasePlugin = "2.0.4" const val logback = "1.2.3" diff --git a/signalc/migrations/changelog.postgresql.sql b/signalc/migrations/changelog.postgresql.sql index acb2dade9a645245a922b3cbaf224be2af6f4d96..cc095d00cfc96eb8bea824eb28aaeed88dd45c26 100644 --- a/signalc/migrations/changelog.postgresql.sql +++ b/signalc/migrations/changelog.postgresql.sql @@ -214,4 +214,9 @@ ALTER TABLE sessions ADD CONSTRAINT pk_sessions PRIMARY KEY (account_id, contact DROP INDEX identities_identity_key_bytes; -- rollback CREATE INDEX identities_identity_key_bytes ON identities (identity_key_bytes); + +-- changeset fdbk:1623280052786-1 failOnError:true +ALTER TABLE contacts ALTER COLUMN phone_number DROP NOT NULL; +-- rollback ALTER TABLE contacts ALTER COLUMN phone_number SET NOT NULL; + -- TODO: uniqueness constraint on contacts account_id_uuid \ No newline at end of file diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt index 0853924f97273ecfaebd18ea280fac108357a849..f2cdce9e19c725d91961b0633c4aa1f01c5b6235 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt @@ -6,7 +6,7 @@ object Contacts: Table(), ContactRecord { override val accountId = varchar("account_id", 255) override val contactId = integer("contact_id").autoIncrement() val uuid = uuid("uuid").nullable() - val phoneNumber = varchar("phone_number", 255) + val phoneNumber = varchar("phone_number", 255).nullable() val profileKeyBytes = binary("profile_key_bytes").nullable() override val primaryKey = PrimaryKey(accountId, contactId) diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt index 56f3ea8de3d3576fe5ef90aeb77589f85d15f640..c6e8a962b8c685ca6a08a0cd23d55a6b6fd53775 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt @@ -159,7 +159,7 @@ class AccountManager(private val app: Application) { val contactAccessKey = app.contactStore.loadProfileKey(accountId, contactId)?.let { UnidentifiedAccess.deriveAccessKeyFrom(it) } ?: run { - logger.error { "Could not derive delivery token for $contactId: no profile key found." } + logger.warn { "Could not derive delivery token for $contactId: no profile key found." } return null } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt index 3232b79565f7549a6f2960b9ab50ae9a060ab4cd..84804c7b468bff144521fd3f418c5d4d4b47d698 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt @@ -194,10 +194,23 @@ class SignalReceiver(private val app: Application) { val dataMessage = contents.dataMessage.orNull() ?: return@launch // drop other message types (eg: typing message, sync message, etc) + if (dataMessage.profileKey.isPresent) { + logger.info { "Profile key: ${dataMessage.profileKey.get()}" } + } else { + logger.info { "Profile key: NOT PRESENT" } + } + + if (dataMessage.isProfileKeyUpdate) { + logger.info { "Profile key update!!!" } + } + contactAddress = contents.sender.asSignalcAddress() val body = dataMessage.body?.orNull() ?: "" val attachments = dataMessage.attachments.orNull() ?: emptyList() + + // TODO: check to see if we already have a profile key for sender, if no then write, if yes then only write if profile key update dataMessage.profileKey.orNull()?.let { + logger.info { "About to store profile key: ${dataMessage.profileKey}" } app.contactStore.storeProfileKey(account.id, contactAddress.identifier, it) } @@ -225,7 +238,7 @@ class SignalReceiver(private val app: Application) { // NOTE: we should only receive N of these upon initiating a conversation with a new contact... // (where N is the number of devices a recipient has) // - q: should this also be able to store a phone number if we only have a UUID? - app.contactStore.storeUuid( + app.contactStore.storeUuidOrPhoneNumber( accountId = account.username, contactPhoneNumber = envelope.sourceE164.get(), contactUuid = UUID.fromString(envelope.sourceUuid.get()), @@ -234,21 +247,22 @@ class SignalReceiver(private val app: Application) { return null } - - // TODO: make this function suspend and async/await!! - private fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) = - // here: app.coroutineScope.async(...) - app.coroutineScope.launch(Concurrency.Dispatcher) { - app.contactStore.create( - accountId = account.username, - phoneNumber = envelope.sourceE164.get(), - uuid = UUID.fromString(envelope.sourceUuid.get()), - ) + private suspend fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) = + app.coroutineScope.async(Concurrency.Dispatcher) { + logger.info { "phoneNumber = ${envelope.sourceE164.get()}, uuid = ${envelope.sourceUuid.get()}" } + app.contactStore.resolveContactIdSuspend(account.username, envelope.sourceIdentifier) ?: run { + app.contactStore.create( + accountId = account.username, + phoneNumber = envelope.sourceE164.get(), + uuid = UUID.fromString(envelope.sourceUuid.get()), + ) + app.signalSender.sendProfileKey(account, envelope.asSignalcAddress()) + } // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation // of which might have depleted our prekey reserves below the level we want to keep on hand // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed! app.accountManager.refreshPreKeysIfDepleted(account) - } + }.await() private suspend fun drop(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? { app.socketSender.send( diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt index f42e66ca5ec04d8b70e3a7326297dac3ece74d81..af49f8ac3a07ab12b89627023ab5d66a6caef28c 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt @@ -118,6 +118,22 @@ class SignalSender(private val app: Application) { .build() ) + suspend fun sendProfileKey( + sender: VerifiedAccount, + recipient: SignalcAddress, + timestamp: Long = TimeUtil.nowInMillis(), + ): SignalcSendResult = + sendDataMessage( + sender, + recipient, + SignalServiceDataMessage + .newBuilder() + .asProfileKeyUpdate(true) + .withTimestamp(timestamp) + .withProfileKey(sender.profileKeyBytes) + .build() + ) + suspend fun setExpiration( sender: VerifiedAccount, recipient: SignalcAddress, diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt index 3ee67148f7c7c209b378f013f7192f660bdba92c..8654976633f13cbb3121623c67ed69c987220b0f 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt @@ -50,38 +50,46 @@ class ContactStore( } }.resultedValues!!.single()[Contacts.contactId] - suspend fun resolveContactIdSuspend(accountId: String, identifier: String): Int = + private suspend fun resolveOrCreateContactIdSuspend(accountId: String, identifier: String): Int = newSuspendedTransaction (dispatcher, db) { - resolveContactId(accountId, identifier) + resolveOrCreateContactId(accountId, identifier) } - fun resolveContactIdBlocking(accountId: String, identifier: String): Int = + fun resolveOrCreateContactIdBlocking(accountId: String, identifier: String): Int = transaction(db) { + resolveOrCreateContactId(accountId, identifier) + } + + private fun resolveOrCreateContactId(accountId: String, identifier: String): Int { + val uuid = parseUuid(identifier) + return resolveContactId(accountId, identifier) ?: runBlocking { + if(uuid == null) { + create(accountId, identifier, null) + } else { + create(accountId, null, uuid) + } + } + } + + suspend fun resolveContactIdSuspend(accountId: String, identifier: String): Int? = + newSuspendedTransaction (dispatcher, db) { resolveContactId(accountId, identifier) } - private fun resolveContactId(accountId: String, identifier: String): Int { + // resolve contact id when one identifier available + private fun resolveContactId(accountId: String, identifier: String): Int? { val uuid = parseUuid(identifier) return run { uuid ?.let { - logger.debug { "Found uuid: $uuid" } + // logger.debug { "Found uuid: $uuid" } Contacts.select { (Contacts.accountId eq accountId).and(Contacts.uuid eq it) } } ?: Contacts.select { - logger.debug { "Found phone number: $identifier" } + // logger.debug { "Found phone number: $identifier" } (Contacts.accountId eq accountId).and(Contacts.phoneNumber eq identifier) } - }.single().let { it[Contacts.contactId] } -// }.singleOrNull() -// ?.let { -// it[Contacts.contactId] -// } -// ?: runBlocking { -// logger.debug { "Creating new contact w/ identifier: $uuid" } -// val phoneNumber: String? = if(uuid == null) identifier else null -// create(accountId, phoneNumber, uuid) -// } + }.singleOrNull()?.let { it[Contacts.contactId] } } private fun parseUuid(identifier: String): UUID? = @@ -91,13 +99,17 @@ class ContactStore( null } - suspend fun storeUuid(accountId: String, contactPhoneNumber: String, contactUuid: UUID) { - // TODO: this currently only adds a uuid to a phonenumber-based contact record - // it shoudl be able to add either a uuid or aphonenumber - newSuspendedTransaction(dispatcher, db) { + suspend fun storeUuidOrPhoneNumber(accountId: String, contactPhoneNumber: String, contactUuid: UUID) { + newSuspendedTransaction(dispatcher, db){ val contactId = resolveContactId(accountId, contactPhoneNumber) + ?: resolveContactId(accountId, contactUuid.toString()) + ?: run { + logger.warn { "No contact ID found for phone number: $contactPhoneNumber, uuid: $contactUuid" } + return@newSuspendedTransaction + } Contacts.updateByContactId(accountId, contactId) { it[Contacts.uuid] = contactUuid + it[Contacts.phoneNumber] = contactPhoneNumber } } } @@ -107,7 +119,7 @@ class ContactStore( logger.warn { "Received profile key of invalid size ${profileKey.size} for account: $contactIdentifier" } return } - val contactId = resolveContactIdSuspend(accountId, contactIdentifier) + val contactId = resolveOrCreateContactIdSuspend(accountId, contactIdentifier) return newSuspendedTransaction(dispatcher, db) { Contacts.updateByContactId(accountId, contactId) { it[Contacts.profileKeyBytes] = profileKey @@ -117,9 +129,9 @@ class ContactStore( suspend fun loadProfileKey(accountId: String, contactIdentifier: String): ProfileKey? = newSuspendedTransaction(dispatcher, db) { - val contactId = resolveContactIdSuspend(accountId, contactIdentifier) - Contacts.findByContactId(accountId, contactId)?.let { - ProfileKey(it[Contacts.profileKeyBytes]) + val contactId = resolveOrCreateContactIdSuspend(accountId, contactIdentifier) + Contacts.findByContactId(accountId, contactId)?.let { resultRow -> + resultRow[Contacts.profileKeyBytes]?.let{ ProfileKey(it) } } } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt index 516830e81267bce7376694e84b88f73b298ed38f..22473ecaf762a03417591572116f4e70f101d4ad 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt @@ -28,9 +28,7 @@ class ProtocolStore(val app: Application) { fun of(account: Account): AccountProtocolStore = AccountProtocolStore( db, account.username, - // TODO: if we need to resolve race in signal sender for first incoming message, - // we might need to construct this in the constructor such that it consumes a session lock (for the account)... - app.contactStore::resolveContactIdBlocking + app.contactStore::resolveOrCreateContactIdBlocking ) fun countOwnIdentities(): Long =