Merge remote-tracking branch 'upstream/dev' into closed_groups

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
#	app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
#	app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
#	app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt
#	app/src/main/res/drawable/profile_picture_view_large_background.xml
#	app/src/main/res/layout/dialog_download.xml
#	app/src/main/res/layout/view_untrusted_attachment.xml
#	app/src/main/res/values/strings.xml
#	app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt
#	libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
#	libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt
#	libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt
#	libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
#	libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java
#	libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt
This commit is contained in:
0x330a
2023-08-23 16:45:24 +10:00
524 changed files with 16783 additions and 23738 deletions

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="screen_security_default">false</bool>
</resources>

View File

@@ -33,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
foreground.setScaleType(ImageView.ScaleType.CENTER_CROP);
if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);

View File

@@ -2,12 +2,14 @@ package org.session.libsession.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.ConfigBase
import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
@@ -30,6 +32,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
interface StorageProtocol {
@@ -37,7 +40,10 @@ interface StorageProtocol {
fun getUserPublicKey(): String?
fun getUserX25519KeyPair(): ECKeyPair
fun getUserProfile(): Profile
fun setUserProfilePictureURL(newProfilePicture: String)
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
fun clearUserPic()
// Signal
fun getOrGenerateRegistrationID(): Int
@@ -49,9 +55,11 @@ interface StorageProtocol {
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
fun getGroupAvatarDownloadJob(server: String, room: String): Job?
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
fun getConfigSyncJob(destination: Destination): Job?
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
fun isJobCanceled(job: Job): Boolean
fun cancelPendingMessageSendJobs(threadID: Long)
// Authorization
fun getAuthToken(room: String, server: String): String?
@@ -67,7 +75,7 @@ interface StorageProtocol {
fun updateOpenGroup(openGroup: OpenGroup)
fun getOpenGroup(threadId: Long): OpenGroup?
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
fun onOpenGroupAdded(server: String)
fun onOpenGroupAdded(server: String, room: String)
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
fun getOpenGroup(room: String, server: String): OpenGroup?
@@ -80,6 +88,7 @@ interface StorageProtocol {
// Open Group Metadata
fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun removeProfilePicture(groupID: String)
fun hasDownloadedProfilePicture(groupID: String): Boolean
fun setUserCount(room: String, server: String, newValue: Int)
@@ -105,16 +114,21 @@ interface StorageProtocol {
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
fun markAsResyncing(timestamp: Long, author: String)
fun markAsSyncing(timestamp: Long, author: String)
fun markAsSending(timestamp: Long, author: String)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
fun clearErrorMessage(messageID: Long)
fun setMessageServerHash(messageID: Long, serverHash: String)
// Closed Groups
fun getGroup(groupID: String): GroupRecord?
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair)
fun updateGroupConfig(groupPublicKey: String)
fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean)
fun getZombieMembers(groupID: String): Set<String>
@@ -125,7 +139,7 @@ interface StorageProtocol {
fun getAllActiveClosedGroupPublicKeys(): Set<String>
fun addClosedGroupPublicKey(groupPublicKey: String)
fun removeClosedGroupPublicKey(groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
@@ -136,18 +150,20 @@ interface StorageProtocol {
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long)
fun setExpirationTimer(groupID: String, duration: Int)
fun setExpirationTimer(address: String, duration: Int)
// Groups
fun getAllGroups(): List<GroupRecord>
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
// Settings
fun setProfileSharing(address: Address, value: Boolean)
// Thread
fun getOrCreateThreadIdFor(address: Address): Long
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long?
fun getThreadId(publicKeyOrOpenGroupID: String): Long?
fun getThreadId(openGroup: OpenGroup): Long?
fun getThreadId(address: Address): Long?
fun getThreadId(recipient: Recipient): Long?
fun getThreadIdForMms(mmsId: Long): Long
@@ -155,8 +171,10 @@ interface StorageProtocol {
fun trimThread(threadID: Long, threadLimit: Int)
fun trimThreadBefore(threadID: Long, timestamp: Long)
fun getMessageCount(threadID: Long): Long
fun setThreadPinned(threadID: Long, isPinned: Boolean)
fun isThreadPinned(threadID: Long): Boolean
fun setPinned(threadID: Long, isPinned: Boolean)
fun isPinned(threadID: Long): Boolean
fun deleteConversation(threadID: Long)
fun setThreadDate(threadId: Long, newDate: Long)
fun clearMessages(threadID: Long, fromUser: Address? = null): Boolean
fun clearMedia(threadID: Long, fromUser: Address? = null): Boolean
@@ -166,6 +184,7 @@ interface StorageProtocol {
fun setContact(contact: Contact)
fun getRecipientForThread(threadId: Long): Recipient?
fun getRecipientSettings(address: Address): RecipientSettings?
fun addLibSessionContacts(contacts: List<LibSessionContact>)
fun hasAutoDownloadFlagBeenSet(recipient: Recipient): Boolean
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
fun shouldAutoDownloadAttachments(recipient: Recipient): Boolean
@@ -179,13 +198,14 @@ interface StorageProtocol {
/**
* Returns the ID of the `TSIncomingMessage` that was constructed.
*/
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runIncrement: Boolean, runThreadUpdate: Boolean): Long?
fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean)
fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int)
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runThreadUpdate: Boolean): Long?
fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false)
fun getLastSeen(threadId: Long): Long
fun updateThread(threadId: Long, unarchive: Boolean)
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
fun insertMessageRequestResponse(response: MessageRequestResponse)
fun setRecipientApproved(recipient: Recipient, approved: Boolean)
fun getRecipientApproved(address: Address): Boolean
fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean)
fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long)
fun conversationHasOutgoing(userPublicKey: String): Boolean
@@ -205,6 +225,12 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean)
fun unblock(toUnblock: List<Recipient>)
fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
fun setRecipientHash(recipient: Recipient, recipientHash: String?)
fun blockedContacts(): List<Recipient>
// Shared configs
fun notifyConfigUpdates(forConfigObject: ConfigBase)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
}

View File

@@ -4,12 +4,16 @@ import android.content.Context
import com.goterl.lazysodium.utils.KeyPair
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.Device
class MessagingModuleConfiguration(
val context: Context,
val storage: StorageProtocol,
val device: Device,
val messageDataProvider: MessageDataProvider,
val getUserED25519KeyPair: ()-> KeyPair?
val getUserED25519KeyPair: () -> KeyPair?,
val configFactory: ConfigFactoryProtocol
) {
companion object {

View File

@@ -16,15 +16,6 @@ object FileServerApi {
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
const val server = "http://filev2.getsession.org"
const val maxFileSize = 10_000_000 // 10 MB
/**
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
*/
const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.")

View File

@@ -42,7 +42,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
}
override fun execute() {
override suspend fun execute(dispatcherName: String) {
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val threadID = storage.getThreadIdForMms(databaseMessageID)
@@ -59,7 +59,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
Log.d("AttachmentDownloadJob", "Setting attachment state = failed, don't have attachment")
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
}
this.handlePermanentFailure(exception)
this.handlePermanentFailure(dispatcherName, exception)
} else if (exception == Error.DuplicateData) {
attachment?.let { id ->
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
@@ -68,7 +68,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
messageDataProvider.setAttachmentState(AttachmentState.DONE, AttachmentId(attachmentID,0), databaseMessageID)
}
this.handleSuccess()
this.handleSuccess(dispatcherName)
} else {
if (failureCount + 1 >= maxFailureCount) {
attachment?.let { id ->
@@ -79,7 +79,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
}
}
this.handleFailure(exception)
this.handleFailure(dispatcherName, exception)
}
}
@@ -145,7 +145,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
Log.d("AttachmentDownloadJob", "deleting tempfile")
tempFile.delete()
Log.d("AttachmentDownloadJob", "succeeding job")
handleSuccess()
handleSuccess(dispatcherName)
} catch (e: Exception) {
Log.e("AttachmentDownloadJob", "Error processing attachment download", e)
tempFile?.delete()
@@ -164,17 +164,17 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
}
}
private fun handleSuccess() {
private fun handleSuccess(dispatcherName: String) {
Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.")
delegate?.handleJobSucceeded(this)
delegate?.handleJobSucceeded(this, dispatcherName)
}
private fun handlePermanentFailure(e: Exception) {
delegate?.handleJobFailedPermanently(this, e)
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
}
private fun handleFailure(e: Exception) {
delegate?.handleJobFailed(this, e)
private fun handleFailure(dispatcherName: String, e: Exception) {
delegate?.handleJobFailed(this, dispatcherName, e)
}
private fun createTempFile(): File {

View File

@@ -16,7 +16,11 @@ import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.session.libsession.utilities.UploadResult
import org.session.libsignal.messages.SignalServiceAttachmentStream
import org.session.libsignal.streams.*
import org.session.libsignal.streams.AttachmentCipherOutputStream
import org.session.libsignal.streams.AttachmentCipherOutputStreamFactory
import org.session.libsignal.streams.DigestingRequestBody
import org.session.libsignal.streams.PaddingInputStream
import org.session.libsignal.streams.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PushAttachmentData
import org.session.libsignal.utilities.Util
@@ -45,29 +49,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
}
override fun execute() {
override suspend fun execute(dispatcherName: String) {
try {
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
?: return handleFailure(dispatcherName, Error.NoAttachment)
val openGroup = storage.getOpenGroup(threadID.toLong())
if (openGroup != null) {
val keyAndResult = upload(attachment, openGroup.server, false) {
OpenGroupApi.upload(it, openGroup.room, openGroup.server)
}
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
} else {
val keyAndResult = upload(attachment, FileServerApi.server, true) {
FileServerApi.upload(it)
}
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
}
} catch (e: java.lang.Exception) {
if (e == Error.NoAttachment) {
this.handlePermanentFailure(e)
this.handlePermanentFailure(dispatcherName, e)
} else {
this.handleFailure(e)
this.handleFailure(dispatcherName, e)
}
}
}
@@ -104,9 +108,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
return Pair(key, UploadResult(id, "${server}/file/$id", digest))
}
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
private fun handleSuccess(dispatcherName: String, attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
Log.d(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this)
delegate?.handleJobSucceeded(this, dispatcherName)
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
if (attachment.contentType.startsWith("audio/")) {
@@ -144,16 +148,16 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
storage.resumeMessageSendJobIfNeeded(messageSendJobID)
}
private fun handlePermanentFailure(e: Exception) {
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e)
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
failAssociatedMessageSendJob(e)
}
private fun handleFailure(e: Exception) {
private fun handleFailure(dispatcherName: String, e: Exception) {
Log.w(TAG, "Attachment upload failed due to error: $this.")
delegate?.handleJobFailed(this, e)
delegate?.handleJobFailed(this, dispatcherName, e)
if (failureCount + 1 >= maxFailureCount) {
failAssociatedMessageSendJob(e)
}

View File

@@ -3,9 +3,7 @@ package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.Log
@@ -29,32 +27,25 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
return "$server.$room"
}
override fun execute() {
override suspend fun execute(dispatcherName: String) {
try {
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
val storage = MessagingModuleConfiguration.shared.storage
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
if (allOpenGroups.contains(openGroup.joinUrl())) {
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
delegate?.handleJobFailed(this, DuplicateGroupException())
delegate?.handleJobFailed(this, dispatcherName, DuplicateGroupException())
return
}
// get image
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
val info = storage.addOpenGroup(openGroup.joinUrl())
val imageId = info?.imageId
if (imageId != null) {
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server))
}
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
storage.onOpenGroupAdded(openGroup.server)
storage.addOpenGroup(openGroup.joinUrl())
storage.onOpenGroupAdded(openGroup.server, openGroup.room)
} catch (e: Exception) {
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
delegate?.handleJobFailed(this, e)
delegate?.handleJobFailed(this, dispatcherName, e)
return
}
Log.d("Loki", "Group added successfully")
delegate?.handleJobSucceeded(this)
delegate?.handleJobSucceeded(this, dispatcherName)
}
override fun serialize(): Data = Data.Builder()

View File

@@ -7,15 +7,26 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.ParsedMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.*
import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.messaging.sending_receiving.handle
import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions
import org.session.libsession.messaging.sending_receiving.handleUnsendRequest
import org.session.libsession.messaging.sending_receiving.handleVisibleMessage
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
@@ -49,6 +60,9 @@ class BatchMessageReceiveJob(
const val BATCH_DEFAULT_NUMBER = 512
// used for processing messages that don't have a thread and shouldn't create one
const val NO_THREAD_MAPPING = -1L
// Keys used for database storage
private val NUM_MESSAGES_KEY = "numMessages"
private val DATA_KEY = "data"
@@ -57,35 +71,47 @@ class BatchMessageReceiveJob(
private val OPEN_GROUP_ID_KEY = "open_group_id"
}
private fun getThreadId(message: Message, storage: StorageProtocol): Long {
val senderOrSync = when (message) {
is VisibleMessage -> message.syncTarget ?: message.sender!!
is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!!
else -> message.sender!!
private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean {
val message = parsedMessage.message
if (message is VisibleMessage) return true
else { // message is control message otherwise
return when(message) {
is SharedConfigurationMessage -> false
is ClosedGroupControlMessage -> false // message.kind is ClosedGroupControlMessage.Kind.New && !message.isSenderSelf
is DataExtractionNotification -> false
is MessageRequestResponse -> false
is ExpirationTimerUpdate -> false
is ConfigurationMessage -> false
is TypingIndicator -> false
is UnsendRequest -> false
is ReadReceipt -> false
is CallMessage -> false // TODO: maybe
else -> false // shouldn't happen, or I guess would be Visible
}
}
return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID)
}
override fun execute() {
executeAsync().get()
override suspend fun execute(dispatcherName: String) {
executeAsync(dispatcherName).get()
}
fun executeAsync(): Promise<Unit, Exception> {
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
return task {
val threadMap = mutableMapOf<Long, MutableList<ParsedMessage>>()
val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context
val localUserPublicKey = storage.getUserPublicKey()
val serverPublicKey = openGroupID?.let { storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) }
val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys()
// parse and collect IDs
messages.forEach { messageParameters ->
val (data, serverHash, openGroupMessageServerID) = messageParameters
try {
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups)
message.serverHash = serverHash
val threadID = getThreadId(message, storage)
val parsedParams = ParsedMessage(messageParameters, message, proto)
val threadID = Message.getThreadId(message, openGroupID, storage, shouldCreateThread(parsedParams)) ?: NO_THREAD_MAPPING
if (!threadMap.containsKey(threadID)) {
threadMap[threadID] = mutableListOf(parsedParams)
} else {
@@ -94,19 +120,19 @@ class BatchMessageReceiveJob(
} catch (e: Exception) {
when (e) {
is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}")
Log.i(TAG, "Couldn't receive message, failed with error: ${e.message} (id: $id)")
}
is MessageReceiver.Error -> {
if (!e.isRetryable) {
Log.e(TAG, "Couldn't receive message, failed permanently", e)
Log.e(TAG, "Couldn't receive message, failed permanently (id: $id)", e)
}
else {
Log.e(TAG, "Couldn't receive message, failed", e)
Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
failures += messageParameters
}
}
else -> {
Log.e(TAG, "Couldn't receive message, failed", e)
Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
failures += messageParameters
}
}
@@ -115,92 +141,118 @@ class BatchMessageReceiveJob(
// iterate over threads and persist them (persistence is the longest constant in the batch process operation)
runBlocking(Dispatchers.IO) {
val deferredThreadMap = threadMap.entries.map { (threadId, messages) ->
async {
// The LinkedHashMap should preserve insertion order
val messageIds = linkedMapOf<Long, Pair<Boolean, Boolean>>()
messages.forEach { (parameters, message, proto) ->
try {
when (message) {
is VisibleMessage -> {
val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID,
runIncrement = false,
runThreadUpdate = false,
runProfileUpdate = true
)
if (messageId != null && message.reaction == null) {
val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
messageIds[messageId] = Pair(
(message.sender == localUserPublicKey || isUserBlindedSender),
message.hasMention
fun processMessages(threadId: Long, messages: List<ParsedMessage>) = async {
// The LinkedHashMap should preserve insertion order
val messageIds = linkedMapOf<Long, Pair<Boolean, Boolean>>()
val myLastSeen = storage.getLastSeen(threadId)
var newLastSeen = if (myLastSeen == -1L) 0 else myLastSeen
messages.forEach { (parameters, message, proto) ->
try {
when (message) {
is VisibleMessage -> {
val isUserBlindedSender =
message.sender == serverPublicKey?.let {
SodiumUtilities.blindedKeyPair(
it,
MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
)
}?.let {
SessionId(
IdPrefix.BLINDED, it.publicKey.asBytes
).hexString
}
parameters.openGroupMessageServerID?.let {
MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions)
val sentTimestamp = message.sentTimestamp!!
if (message.sender == localUserPublicKey || isUserBlindedSender) {
if (sentTimestamp > newLastSeen) {
newLastSeen =
sentTimestamp // use sent timestamp here since that is technically the last one we have
}
}
val messageId = MessageReceiver.handleVisibleMessage(
message, proto, openGroupID, threadId,
runThreadUpdate = false,
runProfileUpdate = true
)
is UnsendRequest -> {
val deletedMessageId = MessageReceiver.handleUnsendRequest(message)
// If we removed a message then ensure it isn't in the 'messageIds'
if (deletedMessageId != null) {
messageIds.remove(deletedMessageId)
}
if (messageId != null && message.reaction == null) {
messageIds[messageId] = Pair(
(message.sender == localUserPublicKey || isUserBlindedSender),
message.hasMention
)
}
parameters.openGroupMessageServerID?.let {
MessageReceiver.handleOpenGroupReactions(
threadId,
it,
parameters.reactions
)
}
}
else -> MessageReceiver.handle(message, proto, openGroupID)
}
} catch (e: Exception) {
Log.e(TAG, "Couldn't process message.", e)
if (e is MessageReceiver.Error && !e.isRetryable) {
Log.e(TAG, "Message failed permanently",e)
} else {
Log.e(TAG, "Message failed",e)
failures += parameters
is UnsendRequest -> {
val deletedMessageId =
MessageReceiver.handleUnsendRequest(message)
// If we removed a message then ensure it isn't in the 'messageIds'
if (deletedMessageId != null) {
messageIds.remove(deletedMessageId)
}
}
else -> MessageReceiver.handle(message, proto, threadId, openGroupID)
}
} catch (e: Exception) {
Log.e(TAG, "Couldn't process message (id: $id)", e)
if (e is MessageReceiver.Error && !e.isRetryable) {
Log.e(TAG, "Message failed permanently (id: $id)", e)
} else {
Log.e(TAG, "Message failed (id: $id)", e)
failures += parameters
}
}
// increment unreads, notify, and update thread
val unreadFromMine = messageIds.map { it.value.first }.indexOfLast { it }
var trueUnreadCount = messageIds.filter { !it.value.first }.size
var trueUnreadMentionCount = messageIds.filter { !it.value.first && it.value.second }.size
if (unreadFromMine >= 0) {
storage.markConversationAsRead(threadId, false)
val trueUnreadIds = messageIds.keys.toList().subList(unreadFromMine + 1, messageIds.keys.count())
trueUnreadCount = trueUnreadIds.size
trueUnreadMentionCount = messageIds
.filter { trueUnreadIds.contains(it.key) && !it.value.first && it.value.second }
.size
}
if (trueUnreadCount > 0) {
storage.incrementUnread(threadId, trueUnreadCount, trueUnreadMentionCount)
}
storage.updateThread(threadId, true)
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)
}
// increment unreads, notify, and update thread
// last seen will be the current last seen if not changed (re-computes the read counts for thread record)
// might have been updated from a different thread at this point
val currentLastSeen = storage.getLastSeen(threadId).let { if (it == -1L) 0 else it }
if (currentLastSeen > newLastSeen) {
newLastSeen = currentLastSeen
}
if (newLastSeen > 0 || currentLastSeen == 0L) {
storage.markConversationAsRead(threadId, newLastSeen, force = true)
}
storage.updateThread(threadId, true)
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)
}
val withoutDefault = threadMap.entries.filter { it.key != NO_THREAD_MAPPING }
val noThreadMessages = threadMap[NO_THREAD_MAPPING] ?: listOf()
val deferredThreadMap = withoutDefault.map { (threadId, messages) ->
processMessages(threadId, messages)
}
// await all thread processing
deferredThreadMap.awaitAll()
if (noThreadMessages.isNotEmpty()) {
processMessages(NO_THREAD_MAPPING, noThreadMessages).await()
}
}
if (failures.isEmpty()) {
handleSuccess()
handleSuccess(dispatcherName)
} else {
handleFailure()
handleFailure(dispatcherName)
}
}
}
private fun handleSuccess() {
this.delegate?.handleJobSucceeded(this)
private fun handleSuccess(dispatcherName: String) {
Log.i(TAG, "Completed processing of ${messages.size} messages (id: $id)")
this.delegate?.handleJobSucceeded(this, dispatcherName)
}
private fun handleFailure() {
this.delegate?.handleJobFailed(this, Exception("One or more jobs resulted in failure"))
private fun handleFailure(dispatcherName: String) {
Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully) (id: $id)")
this.delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure"))
}
override fun serialize(): Data {
@@ -233,10 +285,9 @@ class BatchMessageReceiveJob(
val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
val parameters = (0 until numMessages).map { index ->
val data = contents[index]
val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it }
val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it }
MessageReceiveParameters(data, serverHash, serverId)
MessageReceiveParameters(contents[index], serverHash, serverId)
}
return BatchMessageReceiveJob(parameters, openGroupID)

View File

@@ -0,0 +1,206 @@
package org.session.libsession.messaging.jobs
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor
import nl.komponents.kovenant.functional.bind
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.RawResponse
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import java.util.concurrent.atomic.AtomicBoolean
// only contact (self) and closed group destinations will be supported
data class ConfigurationSyncJob(val destination: Destination): Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 10
val shouldRunAgain = AtomicBoolean(false)
override suspend fun execute(dispatcherName: String) {
val storage = MessagingModuleConfiguration.shared.storage
val forcedConfig = TextSecurePreferences.hasForcedNewConfig(MessagingModuleConfiguration.shared.context)
val currentTime = SnodeAPI.nowWithOffset
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
val userPublicKey = storage.getUserPublicKey()
val delegate = delegate
if (destination is Destination.ClosedGroup // TODO: closed group configs will be handled in closed group feature
// if we haven't enabled the new configs don't run
|| !ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)
// if we don't have a user ed key pair for signing updates
|| userEdKeyPair == null
// this will be useful to not handle null delegate cases
|| delegate == null
// check our local identity key exists
|| userPublicKey.isNullOrEmpty()
// don't allow pushing configs for non-local user
|| (destination is Destination.Contact && destination.publicKey != userPublicKey)
) {
Log.w(TAG, "No need to run config sync job, TODO")
return delegate?.handleJobSucceeded(this, dispatcherName) ?: Unit
}
// configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc
val configFactory = MessagingModuleConfiguration.shared.configFactory
// get latest states, filter out configs that don't need push
val configsRequiringPush = configFactory.getUserConfigs().filter { config -> config.needsPush() }
// don't run anything if we don't need to push anything
if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName)
// need to get the current hashes before we call `push()`
val toDeleteHashes = mutableListOf<String>()
// allow null results here so the list index matches configsRequiringPush
val sentTimestamp: Long = SnodeAPI.nowWithOffset
val batchObjects: List<Pair<SharedConfigurationMessage, SnodeAPI.SnodeBatchRequestInfo>?> = configsRequiringPush.map { config ->
val (data, seqNo, obsoleteHashes) = config.push()
toDeleteHashes += obsoleteHashes
SharedConfigurationMessage(config.protoKindFor(), data, seqNo) to config
}.map { (message, config) ->
// return a list of batch request objects
val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true)
val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo(
destination.destinationPublicKey(),
config.configNamespace(),
snodeMessage
) ?: return@map null // this entry will be null otherwise
message to authenticated // to keep track of seqNo for calling confirmPushed later
}
val toDeleteRequest = toDeleteHashes.let { toDeleteFromAllNamespaces ->
if (toDeleteFromAllNamespaces.isEmpty()) null
else SnodeAPI.buildAuthenticatedDeleteBatchInfo(destination.destinationPublicKey(), toDeleteFromAllNamespaces)
}
if (batchObjects.any { it == null }) {
// stop running here, something like a signing error occurred
return delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("One or more requests had a null batch request info"))
}
val allRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
allRequests += batchObjects.requireNoNulls().map { (_, request) -> request }
// add in the deletion if we have any hashes
if (toDeleteRequest != null) {
allRequests += toDeleteRequest
Log.d(TAG, "Including delete request for current hashes")
}
val batchResponse = SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode ->
SnodeAPI.getRawBatchResponse(
snode,
destination.destinationPublicKey(),
allRequests,
sequence = true
)
}
try {
val rawResponses = batchResponse.get()
@Suppress("UNCHECKED_CAST")
val responseList = (rawResponses["results"] as List<RawResponse>)
// we are always adding in deletions at the end
val deletionResponse = if (toDeleteRequest != null && responseList.isNotEmpty()) responseList.last() else null
val deletedHashes = deletionResponse?.let {
@Suppress("UNCHECKED_CAST")
// get the sub-request body
(deletionResponse["body"] as? RawResponse)?.let { body ->
// get the swarm dict
body["swarm"] as? RawResponse
}?.mapValues { (_, swarmDict) ->
// get the deleted values from dict
((swarmDict as? RawResponse)?.get("deleted") as? List<String>)?.toSet() ?: emptySet()
}?.values?.reduce { acc, strings ->
// create an intersection of all deleted hashes (common between all swarm nodes)
acc intersect strings
}
} ?: emptySet()
// at this point responseList index should line up with configsRequiringPush index
configsRequiringPush.forEachIndexed { index, config ->
val (toPushMessage, _) = batchObjects[index]!!
val response = responseList[index]
val responseBody = response["body"] as? RawResponse
val insertHash = responseBody?.get("hash") as? String ?: run {
Log.w(TAG, "No hash returned for the configuration in namespace ${config.configNamespace()}")
return@forEachIndexed
}
Log.d(TAG, "Hash ${insertHash.take(4)} returned from store request for new config")
// confirm pushed seqno
val thisSeqNo = toPushMessage.seqNo
config.confirmPushed(thisSeqNo, insertHash)
Log.d(TAG, "Successfully removed the deleted hashes from ${config.javaClass.simpleName}")
// dump and write config after successful
if (config.needsDump()) { // usually this will be true?
configFactory.persist(config, toPushMessage.sentTimestamp ?: sentTimestamp)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error performing batch request", e)
return delegate.handleJobFailed(this, dispatcherName, e)
}
delegate.handleJobSucceeded(this, dispatcherName)
if (shouldRunAgain.get() && storage.getConfigSyncJob(destination) == null) {
// reschedule if something has updated since we started this job
JobQueue.shared.add(ConfigurationSyncJob(destination))
}
}
fun Destination.destinationPublicKey(): String = when (this) {
is Destination.Contact -> publicKey
is Destination.ClosedGroup -> groupPublicKey
else -> throw NullPointerException("Not public key for this destination")
}
override fun serialize(): Data {
val (type, address) = when (destination) {
is Destination.Contact -> CONTACT_TYPE to destination.publicKey
is Destination.ClosedGroup -> GROUP_TYPE to destination.groupPublicKey
else -> return Data.EMPTY
}
return Data.Builder()
.putInt(DESTINATION_TYPE_KEY, type)
.putString(DESTINATION_ADDRESS_KEY, address)
.build()
}
override fun getFactoryKey(): String = KEY
companion object {
const val TAG = "ConfigSyncJob"
const val KEY = "ConfigSyncJob"
// Keys used for DB storage
const val DESTINATION_ADDRESS_KEY = "destinationAddress"
const val DESTINATION_TYPE_KEY = "destinationType"
// type mappings
const val CONTACT_TYPE = 1
const val GROUP_TYPE = 2
}
class Factory: Job.Factory<ConfigurationSyncJob> {
override fun create(data: Data): ConfigurationSyncJob? {
if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY)) return null
val address = data.getString(DESTINATION_ADDRESS_KEY)
val destination = when (data.getInt(DESTINATION_TYPE_KEY)) {
CONTACT_TYPE -> Destination.Contact(address)
GROUP_TYPE -> Destination.ClosedGroup(address)
else -> return null
}
return ConfigurationSyncJob(destination)
}
}
}

View File

@@ -3,26 +3,51 @@ package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.GroupUtil
class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: String?) : Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 10
override fun execute() {
override suspend fun execute(dispatcherName: String) {
if (imageId == null) {
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob now requires imageId"))
return
}
val storage = MessagingModuleConfiguration.shared.storage
val imageId = storage.getOpenGroup(room, server)?.imageId ?: return
val openGroup = storage.getOpenGroup(room, server)
if (openGroup == null || storage.getThreadId(openGroup) == null) {
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob openGroup is null"))
return
}
val storedImageId = openGroup.imageId
if (storedImageId == null || storedImageId != imageId) {
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId does not match the OpenGroup"))
return
}
try {
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
// Once the download is complete the imageId might no longer match, so we need to fetch it again just in case
val postDownloadStoredImageId = storage.getOpenGroup(room, server)?.imageId
if (postDownloadStoredImageId == null || postDownloadStoredImageId != imageId) {
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId no longer matches the OpenGroup"))
return
}
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
storage.updateProfilePicture(groupId, bytes)
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
delegate?.handleJobSucceeded(this)
storage.updateTimestampUpdated(groupId, SnodeAPI.nowWithOffset)
delegate?.handleJobSucceeded(this, dispatcherName)
} catch (e: Exception) {
delegate?.handleJobFailed(this, e)
delegate?.handleJobFailed(this, dispatcherName, e)
}
}
@@ -30,6 +55,7 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
return Data.Builder()
.putString(ROOM, room)
.putString(SERVER, server)
.putString(IMAGE_ID, imageId)
.build()
}
@@ -40,14 +66,16 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
private const val ROOM = "room"
private const val SERVER = "server"
private const val IMAGE_ID = "imageId"
}
class Factory : Job.Factory<GroupAvatarDownloadJob> {
override fun create(data: Data): GroupAvatarDownloadJob {
return GroupAvatarDownloadJob(
data.getString(SERVER),
data.getString(ROOM),
data.getString(SERVER)
if (data.hasString(IMAGE_ID)) { data.getString(IMAGE_ID) } else { null }
)
}
}

View File

@@ -17,7 +17,7 @@ interface Job {
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
}
fun execute()
suspend fun execute(dispatcherName: String)
fun serialize(): Data

View File

@@ -2,7 +2,7 @@ package org.session.libsession.messaging.jobs
interface JobDelegate {
fun handleJobSucceeded(job: Job)
fun handleJobFailed(job: Job, error: Exception)
fun handleJobFailedPermanently(job: Job, error: Exception)
fun handleJobSucceeded(job: Job, dispatcherName: String)
fun handleJobFailed(job: Job, dispatcherName: String, error: Exception)
fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception)
}

View File

@@ -53,7 +53,7 @@ class JobQueue : JobDelegate {
}
if (openGroupId.isNullOrEmpty()) {
Log.e("OpenGroupDispatcher", "Open Group ID was null on ${job.javaClass.simpleName}")
handleJobFailedPermanently(job, NullPointerException("Open Group ID was null"))
handleJobFailedPermanently(job, name, NullPointerException("Open Group ID was null"))
} else {
val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) {
Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel")
@@ -94,10 +94,17 @@ class JobQueue : JobDelegate {
}
}
private fun Job.process(dispatcherName: String) {
Log.d(dispatcherName,"processJob: ${javaClass.simpleName}")
private suspend fun Job.process(dispatcherName: String) {
Log.d(dispatcherName,"processJob: ${javaClass.simpleName} (id: $id)")
delegate = this@JobQueue
execute()
try {
execute(dispatcherName)
}
catch (e: Exception) {
Log.d(dispatcherName, "unhandledJobException: ${javaClass.simpleName} (id: $id)")
this@JobQueue.handleJobFailed(this, dispatcherName, e)
}
}
init {
@@ -115,9 +122,10 @@ class JobQueue : JobDelegate {
while (isActive) {
when (val job = queue.receive()) {
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> {
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob, is ConfigurationSyncJob -> {
txQueue.send(job)
}
is RetrieveProfileAvatarJob,
is AttachmentDownloadJob -> {
mediaQueue.send(job)
}
@@ -177,7 +185,7 @@ class JobQueue : JobDelegate {
return
}
if (!pendingJobIds.add(id)) {
Log.e("Loki","tried to re-queue pending/in-progress job")
Log.e("Loki","tried to re-queue pending/in-progress job (id: $id)")
return
}
queue.trySend(job)
@@ -196,7 +204,7 @@ class JobQueue : JobDelegate {
}
}
pendingJobs.sortedBy { it.id }.forEach { job ->
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName} (id: ${job.id}).")
queue.trySend(job) // Offer always called on unlimited capacity
}
}
@@ -217,27 +225,29 @@ class JobQueue : JobDelegate {
GroupAvatarDownloadJob.KEY,
BackgroundGroupAddJob.KEY,
OpenGroupDeleteJob.KEY,
RetrieveProfileAvatarJob.KEY,
ConfigurationSyncJob.KEY,
)
allJobTypes.forEach { type ->
resumePendingJobs(type)
}
}
override fun handleJobSucceeded(job: Job) {
override fun handleJobSucceeded(job: Job, dispatcherName: String) {
val jobId = job.id ?: return
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
pendingJobIds.remove(jobId)
}
override fun handleJobFailed(job: Job, error: Exception) {
override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) {
// Canceled
val storage = MessagingModuleConfiguration.shared.storage
if (storage.isJobCanceled(job)) {
return Log.i("Loki", "${job::class.simpleName} canceled.")
return Log.i("Loki", "${job::class.simpleName} canceled (id: ${job.id}).")
}
// Message send jobs waiting for the attachment to upload
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
Log.i("Loki", "Message send job waiting for attachment upload to finish (id: ${job.id}).")
return
}
@@ -255,21 +265,22 @@ class JobQueue : JobDelegate {
job.failureCount += 1
if (job.failureCount >= job.maxFailureCount) {
handleJobFailedPermanently(job, error)
handleJobFailedPermanently(job, dispatcherName, error)
} else {
storage.persistJob(job)
val retryInterval = getRetryInterval(job)
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
Log.i("Loki", "${job::class.simpleName} failed (id: ${job.id}); scheduling retry (failure count is ${job.failureCount}).")
timer.schedule(delay = retryInterval) {
Log.i("Loki", "Retrying ${job::class.simpleName}.")
Log.i("Loki", "Retrying ${job::class.simpleName} (id: ${job.id}).")
queue.trySend(job)
}
}
}
override fun handleJobFailedPermanently(job: Job, error: Exception) {
override fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception) {
val jobId = job.id ?: return
handleJobFailedPermanently(jobId)
Log.d(dispatcherName, "permanentlyFailedJob: ${javaClass.simpleName} (id: ${job.id})")
}
private fun handleJobFailedPermanently(jobId: String) {

View File

@@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.messaging.sending_receiving.handle
import org.session.libsession.messaging.utilities.Data
@@ -25,46 +26,48 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val
private val OPEN_GROUP_ID_KEY = "open_group_id"
}
override fun execute() {
executeAsync().get()
override suspend fun execute(dispatcherName: String) {
executeAsync(dispatcherName).get()
}
fun executeAsync(): Promise<Unit, Exception> {
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
try {
val isRetry: Boolean = failureCount != 0
val storage = MessagingModuleConfiguration.shared.storage
val serverPublicKey = openGroupID?.let {
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString("."))
storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString("."))
}
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys()
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups)
val threadId = Message.getThreadId(message, this.openGroupID, storage, false)
message.serverHash = serverHash
MessageReceiver.handle(message, proto, this.openGroupID)
this.handleSuccess()
MessageReceiver.handle(message, proto, threadId ?: -1, this.openGroupID)
this.handleSuccess(dispatcherName)
deferred.resolve(Unit)
} catch (e: Exception) {
Log.e(TAG, "Couldn't receive message.", e)
if (e is MessageReceiver.Error && !e.isRetryable) {
Log.e("Loki", "Message receive job permanently failed.", e)
this.handlePermanentFailure(e)
this.handlePermanentFailure(dispatcherName, e)
} else {
Log.e("Loki", "Couldn't receive message.", e)
this.handleFailure(e)
this.handleFailure(dispatcherName, e)
}
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
}
return deferred.promise
}
private fun handleSuccess() {
delegate?.handleJobSucceeded(this)
private fun handleSuccess(dispatcherName: String) {
delegate?.handleJobSucceeded(this, dispatcherName)
}
private fun handlePermanentFailure(e: Exception) {
delegate?.handleJobFailedPermanently(this, e)
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
}
private fun handleFailure(e: Exception) {
delegate?.handleJobFailed(this, e)
private fun handleFailure(dispatcherName: String, e: Exception) {
delegate?.handleJobFailed(this, dispatcherName, e)
}
override fun serialize(): Data {

View File

@@ -10,7 +10,6 @@ import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
@@ -33,7 +32,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
private val DESTINATION_KEY = "destination"
}
override fun execute() {
override suspend fun execute(dispatcherName: String) {
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val message = message as? VisibleMessage
val storage = MessagingModuleConfiguration.shared.storage
@@ -61,12 +60,13 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
}
}
if (attachmentsToUpload.isNotEmpty()) {
this.handleFailure(AwaitingAttachmentUploadException)
this.handleFailure(dispatcherName, AwaitingAttachmentUploadException)
return
} // Wait for all attachments to upload before continuing
}
val promise = MessageSender.send(this.message, this.destination).success {
this.handleSuccess()
val isSync = destination is Destination.Contact && destination.publicKey == sender
val promise = MessageSender.send(this.message, this.destination, isSync).success {
this.handleSuccess(dispatcherName)
}.fail { exception ->
var logStacktrace = true
@@ -75,14 +75,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
is HTTP.HTTPRequestFailedException -> {
logStacktrace = false
if (exception.statusCode == 429) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
if (exception.statusCode == 429) { this.handlePermanentFailure(dispatcherName, exception) }
else { this.handleFailure(dispatcherName, exception) }
}
is MessageSender.Error -> {
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
else { this.handleFailure(exception) }
if (!exception.isRetryable) { this.handlePermanentFailure(dispatcherName, exception) }
else { this.handleFailure(dispatcherName, exception) }
}
else -> this.handleFailure(exception)
else -> this.handleFailure(dispatcherName, exception)
}
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
@@ -95,15 +95,15 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
}
}
private fun handleSuccess() {
delegate?.handleJobSucceeded(this)
private fun handleSuccess(dispatcherName: String) {
delegate?.handleJobSucceeded(this, dispatcherName)
}
private fun handlePermanentFailure(error: Exception) {
delegate?.handleJobFailedPermanently(this, error)
private fun handlePermanentFailure(dispatcherName: String, error: Exception) {
delegate?.handleJobFailedPermanently(this, dispatcherName, error)
}
private fun handleFailure(error: Exception) {
private fun handleFailure(dispatcherName: String, error: Exception) {
Log.w(TAG, "Failed to send $message::class.simpleName.")
val message = message as? VisibleMessage
if (message != null) {
@@ -111,7 +111,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
return // The message has been deleted
}
}
delegate?.handleJobFailed(this, error)
delegate?.handleJobFailed(this, dispatcherName, error)
}
override fun serialize(): Data {

View File

@@ -3,20 +3,17 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.notifications.Server
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.Version
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.retryIfNeeded
class NotifyPNServerJob(val message: SnodeMessage) : Job {
@@ -32,34 +29,38 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
private val MESSAGE_KEY = "message"
}
override fun execute() {
val server = PushNotificationAPI.server
override suspend fun execute(dispatcherName: String) {
val server = Server.LEGACY
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
val url = "${server}/notify"
val url = "${server.url}/notify"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
val request = Request.Builder().url(url).post(body).build()
retryIfNeeded(4) {
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
OnionRequestAPI.sendOnionRequest(
request,
server.url,
server.publicKey,
Version.V2
) success { response ->
when (response.code) {
null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
} fail { exception ->
Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
}
}.success {
handleSuccess()
}. fail {
handleFailure(it)
} success {
handleSuccess(dispatcherName)
} fail {
handleFailure(dispatcherName, it)
}
}
private fun handleSuccess() {
delegate?.handleJobSucceeded(this)
private fun handleSuccess(dispatcherName: String) {
delegate?.handleJobSucceeded(this, dispatcherName)
}
private fun handleFailure(error: Exception) {
delegate?.handleJobFailed(this, error)
private fun handleFailure(dispatcherName: String, error: Exception) {
delegate?.handleJobFailed(this, dispatcherName, error)
}
override fun serialize(): Data {

View File

@@ -19,7 +19,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
override var failureCount: Int = 0
override val maxFailureCount: Int = 1
override fun execute() {
override suspend fun execute(dispatcherName: String) {
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val numberToDelete = messageServerIds.size
Log.d(TAG, "Deleting $numberToDelete messages")
@@ -39,10 +39,10 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
}
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
delegate?.handleJobSucceeded(this)
delegate?.handleJobSucceeded(this, dispatcherName)
}
catch (e: Exception) {
delegate?.handleJobFailed(this, e)
delegate?.handleJobFailed(this, dispatcherName, e)
}
}

View File

@@ -0,0 +1,121 @@
package org.session.libsession.messaging.jobs
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DownloadUtilities.downloadFile
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL
import org.session.libsession.utilities.Util.copy
import org.session.libsession.utilities.Util.equals
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.streams.ProfileCipherInputStream
import org.session.libsignal.utilities.Log
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.security.SecureRandom
import java.util.concurrent.ConcurrentSkipListSet
class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipientAddress: Address): Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 3
companion object {
val TAG = RetrieveProfileAvatarJob::class.simpleName
val KEY: String = "RetrieveProfileAvatarJob"
// Keys used for database storage
private const val PROFILE_AVATAR_KEY = "profileAvatar"
private const val RECEIPIENT_ADDRESS_KEY = "recipient"
val errorUrls = ConcurrentSkipListSet<String>()
}
override suspend fun execute(dispatcherName: String) {
val delegate = delegate ?: return
if (profileAvatar in errorUrls) return delegate.handleJobFailed(this, dispatcherName, Exception("Profile URL 404'd this app instance"))
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val recipient = Recipient.from(context, recipientAddress, true)
val profileKey = recipient.resolve().profileKey
if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) {
return delegate.handleJobFailedPermanently(this, dispatcherName, Exception("Recipient profile key is gone!"))
}
// Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so
// it's now limited to just the current user case
if (
recipient.isLocalNumber &&
AvatarHelper.avatarFileExists(context, recipient.resolve().address) &&
equals(profileAvatar, recipient.resolve().profileAvatar)
) {
Log.w(TAG, "Already retrieved profile avatar: $profileAvatar")
return
}
if (profileAvatar.isNullOrEmpty()) {
Log.w(TAG, "Removing profile avatar for: " + recipient.address.serialize())
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
setProfilePictureURL(context, null)
}
AvatarHelper.delete(context, recipient.address)
storage.setProfileAvatar(recipient, null)
return
}
val downloadDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
try {
downloadFile(downloadDestination, profileAvatar)
val avatarStream: InputStream = ProfileCipherInputStream(FileInputStream(downloadDestination), profileKey)
val decryptDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
copy(avatarStream, FileOutputStream(decryptDestination))
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address))
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
setProfilePictureURL(context, profileAvatar)
}
storage.setProfileAvatar(recipient, profileAvatar)
} catch (e: Exception) {
Log.e("Loki", "Failed to download profile avatar", e)
if (failureCount + 1 >= maxFailureCount) {
errorUrls += profileAvatar
}
return delegate.handleJobFailed(this, dispatcherName, e)
} finally {
downloadDestination.delete()
}
return delegate.handleJobSucceeded(this, dispatcherName)
}
override fun serialize(): Data {
return Data.Builder()
.putString(PROFILE_AVATAR_KEY, profileAvatar)
.putString(RECEIPIENT_ADDRESS_KEY, recipientAddress.serialize())
.build()
}
override fun getFactoryKey(): String {
return KEY
}
class Factory: Job.Factory<RetrieveProfileAvatarJob> {
override fun create(data: Data): RetrieveProfileAvatarJob {
val profileAvatar = if (data.hasString(PROFILE_AVATAR_KEY)) { data.getString(PROFILE_AVATAR_KEY) } else { null }
val recipientAddress = Address.fromSerialized(data.getString(RECEIPIENT_ADDRESS_KEY))
return RetrieveProfileAvatarJob(profileAvatar, recipientAddress)
}
}
}

View File

@@ -16,6 +16,7 @@ class SessionJobManagerFactories {
GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(),
BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(),
OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(),
ConfigurationSyncJob.KEY to ConfigurationSyncJob.Factory()
)
}
}

View File

@@ -20,7 +20,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job {
const val THREAD_LENGTH_TRIGGER_SIZE = 2000
}
override fun execute() {
override suspend fun execute(dispatcherName: String) {
val context = MessagingModuleConfiguration.shared.context
val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context)
val storage = MessagingModuleConfiguration.shared.storage
@@ -29,7 +29,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job {
val oldestMessageTime = System.currentTimeMillis() - TRIM_TIME_LIMIT
storage.trimThreadBefore(threadId, oldestMessageTime)
}
delegate?.handleJobSucceeded(this)
delegate?.handleJobSucceeded(this, dispatcherName)
}
override fun serialize(): Data {

View File

@@ -2,6 +2,7 @@ package org.session.libsession.messaging.mentions
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import java.util.*
object MentionsManager {
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
@@ -32,9 +33,9 @@ object MentionsManager {
candidates.sortedBy { it.displayName }
if (query.length >= 2) {
// Filter out any non-matching candidates
candidates = candidates.filter { it.displayName.toLowerCase().contains(query.toLowerCase()) }
candidates = candidates.filter { it.displayName.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) }
// Sort based on where in the candidate the query occurs
candidates.sortedBy { it.displayName.toLowerCase().indexOf(query.toLowerCase()) }
candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) }
}
// Return
return candidates

View File

@@ -7,13 +7,13 @@ import org.session.libsignal.utilities.toHexString
sealed class Destination {
class Contact(var publicKey: String) : Destination() {
data class Contact(var publicKey: String) : Destination() {
internal constructor(): this("")
}
class ClosedGroup(var groupPublicKey: String) : Destination() {
data class ClosedGroup(var groupPublicKey: String) : Destination() {
internal constructor(): this("")
}
class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
data class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
internal constructor(): this("", "")
}

View File

@@ -1,5 +1,10 @@
package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.protos.SignalServiceProtos
abstract class Message {
@@ -9,6 +14,7 @@ abstract class Message {
var receivedTimestamp: Long? = null
var recipient: String? = null
var sender: String? = null
var isSenderSelf: Boolean = false
var groupPublicKey: String? = null
var openGroupServerMessageID: Long? = null
var serverHash: String? = null
@@ -16,6 +22,17 @@ abstract class Message {
open val ttl: Long = 14 * 24 * 60 * 60 * 1000
open val isSelfSendValid: Boolean = false
companion object {
fun getThreadId(message: Message, openGroupID: String?, storage: StorageProtocol, shouldCreateThread: Boolean): Long? {
val senderOrSync = when (message) {
is VisibleMessage -> message.syncTarget ?: message.sender!!
is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!!
else -> message.sender!!
}
return storage.getThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID, createThread = shouldCreateThread)
}
}
open fun isValid(): Boolean {
val sentTimestamp = sentTimestamp
if (sentTimestamp != null && sentTimestamp <= 0) { return false }

View File

@@ -122,9 +122,9 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
val displayName = TextSecurePreferences.getProfileName(context) ?: return null
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
val profileKey = ProfileKeyUtil.getProfileKey(context)
val groups = storage.getAllGroups()
val groups = storage.getAllGroups(includeInactive = false)
for (group in groups) {
if (group.isClosedGroup) {
if (group.isClosedGroup && group.isActive) {
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue

View File

@@ -0,0 +1,36 @@
package org.session.libsession.messaging.messages.control
import com.google.protobuf.ByteString
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
class SharedConfigurationMessage(val kind: SharedConfigMessage.Kind, val data: ByteArray, val seqNo: Long): ControlMessage() {
override val ttl: Long = 30 * 24 * 60 * 60 * 1000L
override val isSelfSendValid: Boolean = true
companion object {
fun fromProto(proto: SignalServiceProtos.Content): SharedConfigurationMessage? {
if (!proto.hasSharedConfigMessage()) return null
val sharedConfig = proto.sharedConfigMessage
if (!sharedConfig.hasKind() || !sharedConfig.hasData()) return null
return SharedConfigurationMessage(sharedConfig.kind, sharedConfig.data.toByteArray(), sharedConfig.seqno)
}
}
override fun isValid(): Boolean {
if (!super.isValid()) return false
return data.isNotEmpty() && seqNo >= 0
}
override fun toProto(): SignalServiceProtos.Content? {
val sharedConfigurationMessage = SharedConfigMessage.newBuilder()
.setKind(kind)
.setSeqno(seqNo)
.setData(ByteString.copyFrom(data))
.build()
return SignalServiceProtos.Content.newBuilder()
.setSharedConfigMessage(sharedConfigurationMessage)
.build()
}
}

View File

@@ -11,20 +11,22 @@ import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
class VisibleMessage : Message() {
/** In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
var syncTarget: String? = null
var text: String? = null
val attachmentIDs: MutableList<Long> = mutableListOf()
var quote: Quote? = null
var linkPreview: LinkPreview? = null
var profile: Profile? = null
var openGroupInvitation: OpenGroupInvitation? = null
var reaction: Reaction? = null
/**
* @param syncTarget In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
class VisibleMessage(
var syncTarget: String? = null,
var text: String? = null,
val attachmentIDs: MutableList<Long> = mutableListOf(),
var quote: Quote? = null,
var linkPreview: LinkPreview? = null,
var profile: Profile? = null,
var openGroupInvitation: OpenGroupInvitation? = null,
var reaction: Reaction? = null,
var hasMention: Boolean = false
) : Message() {
override val isSelfSendValid: Boolean = true

View File

@@ -36,7 +36,7 @@ data class OpenGroup(
val server = json.get("server").asText().lowercase(Locale.US)
val displayName = json.get("displayName").asText()
val publicKey = json.get("publicKey").asText()
val imageId = json.get("imageId")?.asText()
val imageId = if (json.hasNonNull("imageId")) { json.get("imageId")?.asText() } else { null }
val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates)

View File

@@ -23,6 +23,7 @@ import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.OnionResponse
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64.decode
import org.session.libsignal.utilities.Base64.encodeBytes
@@ -108,7 +109,24 @@ object OpenGroupApi {
val defaultWrite: Boolean = false,
val upload: Boolean = false,
val defaultUpload: Boolean = false,
)
) {
fun toPollInfo() = RoomPollInfo(
token = token,
activeUsers = activeUsers,
admin = admin,
globalAdmin = globalAdmin,
moderator = moderator,
globalModerator = globalModerator,
read = read,
defaultRead = defaultRead,
defaultAccessible = defaultAccessible,
write = write,
defaultWrite = defaultWrite,
upload = upload,
defaultUpload = defaultUpload,
details = this
)
}
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class PinnedMessage(
@@ -303,7 +321,7 @@ object OpenGroupApi {
val headers = request.headers.toMutableMap()
if (request.isAuthRequired) {
val nonce = sodium.nonce(16)
val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset)
var pubKey = ""
var signature = ByteArray(Sign.BYTES)
var bodyHash = ByteArray(0)

View File

@@ -9,11 +9,13 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.IdPrefix
@@ -33,13 +35,14 @@ object MessageReceiver {
object NoThread: Error("Couldn't find thread for message.")
object SelfSend: Error("Message addressed at self.")
object InvalidGroupPublicKey: Error("Invalid group public key.")
object NoGroupThread: Error("No thread exists for this group.")
object NoGroupKeyPair: Error("Missing group key pair.")
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
internal val isRetryable: Boolean = when (this) {
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
is UnknownEnvelopeType, is InvalidSignature, is NoData,
is SenderBlocked, is SelfSend -> false
is SenderBlocked, is SelfSend, is NoGroupThread -> false
else -> true
}
}
@@ -50,6 +53,7 @@ object MessageReceiver {
isOutgoing: Boolean? = null,
otherBlindedPublicKey: String? = null,
openGroupPublicKey: String? = null,
currentClosedGroups: Set<String>?
): Pair<Message, SignalServiceProtos.Content> {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()
@@ -69,7 +73,7 @@ object MessageReceiver {
} else {
when (envelope.type) {
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
if (IdPrefix.fromValue(envelope.source) == IdPrefix.BLINDED) {
if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) {
openGroupPublicKey ?: throw Error.InvalidGroupPublicKey
otherBlindedPublicKey ?: throw Error.DecryptionFailed
val decryptionResult = MessageDecrypter.decryptBlinded(
@@ -138,6 +142,7 @@ object MessageReceiver {
UnsendRequest.fromProto(proto) ?:
MessageRequestResponse.fromProto(proto) ?:
CallMessage.fromProto(proto) ?:
SharedConfigurationMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: run {
throw Error.UnknownMessage
}
@@ -146,6 +151,9 @@ object MessageReceiver {
if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) {
throw Error.SelfSend
}
if (sender == userPublicKey || isUserBlindedSender) {
message.isSenderSelf = true
}
// Guard against control messages in open groups
if (isOpenGroupMessage && message !is VisibleMessage) {
throw Error.InvalidMessage
@@ -154,7 +162,7 @@ object MessageReceiver {
message.sender = sender
message.recipient = userPublicKey
message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else SnodeAPI.nowWithOffset
message.groupPublicKey = groupPublicKey
message.openGroupServerMessageID = openGroupServerID
// Validate
@@ -166,12 +174,16 @@ object MessageReceiver {
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
// for this issue.
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
if (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet())) {
throw Error.NoGroupThread
}
if ((message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) || message is SharedConfigurationMessage) {
// Allow duplicates in this case to avoid the following situation:
// • The app performed a background poll or received a push notification
// • This method was invoked and the received message timestamps table was updated
// • Processing wasn't finished
// • The user doesn't see the new closed group
// also allow shared configuration messages to be duplicates since we track hashes separately use seqno for conflict resolution
} else {
if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage }
storage.addReceivedMessageTimestamp(envelope.timestamp)

View File

@@ -13,6 +13,7 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.LinkPreview
import org.session.libsession.messaging.messages.visible.Quote
@@ -28,6 +29,7 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.SnodeModule
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.crypto.PushTransportDetails
@@ -61,79 +63,123 @@ object MessageSender {
}
// Convenience
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
sendToOpenGroupDestination(destination, message)
} else {
sendToSnodeDestination(destination, message)
sendToSnodeDestination(destination, message, isSyncMessage)
}
}
// One-on-One Chats & Closed Groups
@Throws(Exception::class)
fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()
// Set the timestamp, sender and recipient
val messageSendTime = SnodeAPI.nowWithOffset
if (message.sentTimestamp == null) {
message.sentTimestamp =
messageSendTime // Visible messages will already have their sent timestamp set
}
message.sender = userPublicKey
when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
else -> throw IllegalStateException("Destination should not be an open group.")
}
val isSelfSend = (message.recipient == userPublicKey)
// Validate the message
if (!message.isValid()) {
throw Error.InvalidMessage
}
// Stop here if this is a self-send, unless it's:
// • a configuration message
// • a sync message
// • a closed group control message of type `new`
var isNewClosedGroupControlMessage = false
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage =
true
if (isSelfSend
&& message !is ConfigurationMessage
&& !isSyncMessage
&& !isNewClosedGroupControlMessage
&& message !is UnsendRequest
&& message !is SharedConfigurationMessage
) {
throw Error.InvalidMessage
}
// Attach the user's profile if needed
if (message is VisibleMessage) {
message.profile = storage.getUserProfile()
}
if (message is MessageRequestResponse) {
message.profile = storage.getUserProfile()
}
// Convert it to protobuf
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
// Serialize the protobuf
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
// Encrypt the serialized protobuf
val ciphertext = when (destination) {
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
is Destination.ClosedGroup -> {
val encryptionKeyPair =
MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(
destination.groupPublicKey
)!!
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
}
else -> throw IllegalStateException("Destination should not be open group.")
}
// Wrap the result
val kind: SignalServiceProtos.Envelope.Type
val senderPublicKey: String
when (destination) {
is Destination.Contact -> {
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
senderPublicKey = ""
}
is Destination.ClosedGroup -> {
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
senderPublicKey = destination.groupPublicKey
}
else -> throw IllegalStateException("Destination should not be open group.")
}
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
// Send the result
return SnodeMessage(
message.recipient!!,
base64EncodedData,
message.ttl,
messageSendTime
)
}
// One-on-One Chats & Closed Groups
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
val promise = deferred.promise
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()
// Set the timestamp, sender and recipient
if (message.sentTimestamp == null) {
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
}
val messageSendTime = System.currentTimeMillis()
// recipient will be set later, so initialize it as a function here
val isSelfSend = { message.recipient == userPublicKey }
message.sender = userPublicKey
val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
handleFailedMessageSend(message, error, isSyncMessage)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
deferred.reject(error)
}
try {
when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
else -> throw IllegalStateException("Destination should not be an open group.")
}
// Validate the message
if (!message.isValid()) { throw Error.InvalidMessage }
// Stop here if this is a self-send, unless it's:
// • a configuration message
// • a sync message
// • a closed group control message of type `new`
var isNewClosedGroupControlMessage = false
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true
if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage && message !is UnsendRequest) {
handleSuccessfulMessageSend(message, destination)
deferred.resolve(Unit)
return promise
}
// Attach the user's profile if needed
if (message is VisibleMessage) {
message.profile = storage.getUserProfile()
}
if (message is MessageRequestResponse) {
message.profile = storage.getUserProfile()
}
// Convert it to protobuf
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
// Serialize the protobuf
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
// Encrypt the serialized protobuf
val ciphertext = when (destination) {
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
is Destination.ClosedGroup -> {
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
}
else -> throw IllegalStateException("Destination should not be open group.")
}
// Wrap the result
val kind: SignalServiceProtos.Envelope.Type
val senderPublicKey: String
val snodeMessage = buildWrappedMessageToSnode(destination, message, isSyncMessage)
// TODO: this might change in future for config messages
val forkInfo = SnodeAPI.forkInfo
val namespaces: List<Int> = when {
@@ -143,29 +189,6 @@ object MessageSender {
&& forkInfo.hasNamespaces() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP, Namespace.DEFAULT)
else -> listOf(Namespace.DEFAULT)
}
when (destination) {
is Destination.Contact -> {
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
senderPublicKey = ""
}
is Destination.ClosedGroup -> {
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
senderPublicKey = destination.groupPublicKey
}
else -> throw IllegalStateException("Destination should not be open group.")
}
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
// Send the result
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", messageSendTime)
}
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
// Send the result
val timestamp = messageSendTime + SnodeAPI.clockOffset
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, timestamp)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime)
}
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
var isSuccess = false
val promiseCount = promises.size
@@ -174,13 +197,23 @@ object MessageSender {
promise.success {
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
isSuccess = true
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeModule.shared.broadcaster.broadcast("messageSent", messageSendTime)
}
val hash = it["hash"] as? String
message.serverHash = hash
handleSuccessfulMessageSend(message, destination, isSyncMessage)
val shouldNotify = ((message is VisibleMessage || message is UnsendRequest || message is CallMessage) && !isSyncMessage)
val shouldNotify: Boolean = when (message) {
is VisibleMessage, is UnsendRequest -> !isSyncMessage
is CallMessage -> {
// Note: Other 'CallMessage' types are too big to send as push notifications
// so only send the 'preOffer' message as a notification
when (message.type) {
SignalServiceProtos.CallMessage.Type.PRE_OFFER -> true
else -> false
}
}
else -> false
}
/*
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
shouldNotify = true
@@ -210,7 +243,7 @@ object MessageSender {
val deferred = deferred<Unit, Exception>()
val storage = MessagingModuleConfiguration.shared.storage
if (message.sentTimestamp == null) {
message.sentTimestamp = System.currentTimeMillis()
message.sentTimestamp = SnodeAPI.nowWithOffset
}
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
var serverCapabilities = listOf<String>()
@@ -361,16 +394,23 @@ object MessageSender {
// • the destination was a contact
// • we didn't sync it already
if (destination is Destination.Contact && !isSyncMessage) {
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
if (message is VisibleMessage) message.syncTarget = destination.publicKey
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
storage.markAsSyncing(message.sentTimestamp!!, userPublicKey)
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
}
}
fun handleFailedMessageSend(message: Message, error: Exception) {
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
val timestamp = message.sentTimestamp!!
val author = message.sender ?: userPublicKey
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)
else storage.markAsSentFailed(timestamp, author, error)
}
// Convenience
@@ -394,29 +434,29 @@ object MessageSender {
@JvmStatic
fun send(message: Message, address: Address) {
val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address)
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
message.threadID = threadID
val destination = Destination.from(address)
val job = MessageSendJob(message, destination)
JobQueue.shared.add(job)
}
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address): Promise<Unit, Exception> {
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> {
val attachmentIDs = MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!)
message.attachmentIDs.addAll(attachmentIDs)
return sendNonDurably(message, address)
return sendNonDurably(message, address, isSyncMessage)
}
fun sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address)
fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> {
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
message.threadID = threadID
val destination = Destination.from(address)
return send(message, destination)
return send(message, destination, isSyncMessage)
}
// Closed groups
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
return create(name, members)
fun createClosedGroup(device: Device, name: String, members: Collection<String>): Promise<String, Exception> {
return create(device, name, members)
}
fun explicitNameChange(groupPublicKey: String, newName: String) {

View File

@@ -8,23 +8,25 @@ import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.Curve
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.Log
import java.util.*
import java.util.concurrent.ConcurrentHashMap
@@ -32,7 +34,11 @@ const val groupSizeLimit = 100
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
fun MessageSender.create(
device: Device,
name: String,
members: Collection<String>
): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
ThreadUtils.queue {
// Prepare
@@ -49,31 +55,46 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
val admins = setOf( userPublicKey )
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), SnodeAPI.nowWithOffset)
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, 0)
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTime)
// Create the thread
storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
val ourPubKey = storage.getUserPublicKey()
for (member in members) {
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
try {
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).get()
} catch (e: Exception) {
// We failed to properly create the group so delete it's associated data (in the past
// we didn't create this data until the messages successfully sent but this resulted
// in race conditions due to the `NEW` message sent to our own swarm)
storage.removeClosedGroupPublicKey(groupPublicKey)
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
storage.deleteConversation(threadID)
deferred.reject(e)
return@queue
}
}
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
// Add the group to the config now that it was successfully created
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
PushRegistryV1.register(device = device, publicKey = userPublicKey)
// Start polling
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
// Fulfill the promise
@@ -83,24 +104,6 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
return deferred.promise
}
fun MessageSender.update(groupPublicKey: String, members: List<String>, name: String) {
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't update nonexistent closed group.")
throw Error.NoThread
}
// Update name if needed
if (name != group.title) { setName(groupPublicKey, name) }
// Add members if needed
val addedMembers = members - group.members.map { it.serialize() }
if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) }
// Remove members if needed
val removedMembers = group.members.map { it.serialize() } - members
if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) }
}
fun MessageSender.setName(groupPublicKey: String, newName: String) {
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
@@ -113,7 +116,7 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
val admins = group.admins.map { it.serialize() }
// Send the update to the group
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
@@ -153,7 +156,7 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
@@ -167,7 +170,7 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
// updates from before that timestamp. By setting the timestamp of the message below to a value
// greater than that of the `MembersAdded` message, we ensure that newly added members ignore
// the `MembersAdded` message.
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
closedGroupControlMessage.sentTimestamp = SnodeAPI.nowWithOffset
send(closedGroupControlMessage, Address.fromSerialized(member))
}
// Notify the user
@@ -208,7 +211,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
@@ -239,18 +242,18 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
val name = group.title
// Send the update to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
closedGroupControlMessage.sentTimestamp = sentTime
storage.setActive(groupID, false)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID), isSyncMessage = false).success {
// Notify the user
val infoType = SignalServiceGroup.Type.QUIT
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
if (notifyUser) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
// Remove the group private key and unsubscribe from PNs
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true)
deferred.resolve(Unit)
}.fail {
storage.setActive(groupID, true)
@@ -282,7 +285,7 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
// Distribute it
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey, SnodeAPI.nowWithOffset)
pendingKeyPairs[groupPublicKey] = Optional.absent()
}
}
@@ -298,11 +301,12 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
}
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
val sentTime = System.currentTimeMillis()
val sentTime = SnodeAPI.nowWithOffset
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
closedGroupControlMessage.sentTimestamp = sentTime
return if (force) {
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
val isSync = MessagingModuleConfiguration.shared.storage.getUserPublicKey() == destination
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination), isSyncMessage = isSync)
} else {
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
null

View File

@@ -1,6 +1,7 @@
package org.session.libsession.messaging.sending_receiving
import android.text.TextUtils
import network.loki.messenger.libsession_util.ConfigBase
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
@@ -23,7 +24,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
@@ -42,6 +43,7 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
@@ -58,7 +60,10 @@ internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
return recipient.isBlocked
}
fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) {
fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, threadId: Long, openGroupID: String?) {
// Do nothing if the message was outdated
if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return }
when (message) {
is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message)
@@ -68,8 +73,8 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
is ConfigurationMessage -> handleConfigurationMessage(message)
is UnsendRequest -> handleUnsendRequest(message)
is MessageRequestResponse -> handleMessageRequestResponse(message)
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID,
runIncrement = true,
is VisibleMessage -> handleVisibleMessage(
message, proto, openGroupID, threadId,
runThreadUpdate = true,
runProfileUpdate = true
)
@@ -77,6 +82,33 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
}
}
fun MessageReceiver.messageIsOutdated(message: Message, threadId: Long, openGroupID: String?): Boolean {
when (message) {
is ReadReceipt -> return false // No visible artifact created so better to keep for more reliable read states
is UnsendRequest -> return false // We should always process the removal of messages just in case
}
// Determine the state of the conversation and the validity of the message
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val threadRecipient = storage.getRecipientForThread(threadId)
val conversationVisibleInConfig = storage.conversationInConfig(
if (message.groupPublicKey == null) threadRecipient?.address?.serialize() else null,
message.groupPublicKey,
openGroupID,
true
)
val canPerformChange = storage.canPerformConfigChange(
if (threadRecipient?.address?.serialize() == userPublicKey) SharedConfigMessage.Kind.USER_PROFILE.name else SharedConfigMessage.Kind.CONTACTS.name,
userPublicKey,
message.sentTimestamp!!
)
// If the thread is visible or the message was sent more recently than the last config message (minus
// buffer period) then we should process the message, if not then the message is outdated
return (!conversationVisibleInConfig && !canPerformChange)
}
// region Control Messages
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
val context = MessagingModuleConfiguration.shared.context
@@ -129,6 +161,7 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
if (message.groupPublicKey != null) return
val storage = MessagingModuleConfiguration.shared.storage
val senderPublicKey = message.sender!!
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT)
is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED)
@@ -149,11 +182,17 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
TextSecurePreferences.setConfigurationMessageSynced(context, true)
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
val isForceSync = TextSecurePreferences.hasForcedNewConfig(context)
val currentTime = SnodeAPI.nowWithOffset
if (ConfigBase.isNewConfigEnabled(isForceSync, currentTime)) {
TextSecurePreferences.setHasLegacyConfig(context, true)
if (!firstTimeSync) return
}
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closedGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) {
// just handle the closed group encryption key pairs to avoid sync'd devices getting out of sync
storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey)
storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey, message.sentTimestamp!!)
} else if (firstTimeSync) {
// only handle new closed group if it's first time sync
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
@@ -166,9 +205,9 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
.replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer)
}) {
if (allV2OpenGroups.contains(openGroup)) continue
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
Log.d("OpenGroup", "All open groups doesn't contain open group")
if (!storage.hasBackgroundGroupAddJob(openGroup)) {
Log.d("OpenGroup", "Doesn't contain background job for $openGroup, adding")
Log.d("OpenGroup", "Doesn't contain background job for open group, adding")
JobQueue.shared.add(BackgroundGroupAddJob(openGroup))
}
}
@@ -182,10 +221,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
&& TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
val profileKey = Base64.encodeBytes(message.profileKey)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
profileManager.setProfileKey(context, recipient, message.profileKey)
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
storage.setUserProfilePictureURL(message.profilePicture!!)
}
profileManager.setProfilePicture(context, recipient, message.profilePicture, message.profileKey)
}
storage.addContacts(message.contacts)
}
@@ -215,24 +251,28 @@ fun handleMessageRequestResponse(message: MessageRequestResponse) {
}
//endregion
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
proto: SignalServiceProtos.Content,
openGroupID: String?,
runIncrement: Boolean,
runThreadUpdate: Boolean,
runProfileUpdate: Boolean): Long? {
fun MessageReceiver.handleVisibleMessage(
message: VisibleMessage,
proto: SignalServiceProtos.Content,
openGroupID: String?,
threadId: Long,
runThreadUpdate: Boolean,
runProfileUpdate: Boolean
): Long? {
val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context
val userPublicKey = storage.getUserPublicKey()
val messageSender: String? = message.sender
// Do nothing if the message was outdated
if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return null }
// Get or create thread
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
// exist. This is intentional, but it's very non-obvious.
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID)
if (threadID < 0) {
val threadID = storage.getThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID, createThread = true)
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
throw MessageReceiver.Error.NoThread
}
?: throw MessageReceiver.Error.NoThread
val threadRecipient = storage.getRecipientForThread(threadID)
val userBlindedKey = openGroupID?.let {
val openGroup = storage.getOpenGroup(threadID) ?: return@let null
@@ -259,9 +299,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
val profileKeyChanged = (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey))
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
profileManager.setProfileKey(context, recipient, newProfileKey!!)
profileManager.setProfilePicture(context, recipient, profile.profilePictureURL, newProfileKey)
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
} else if (newProfileKey == null || newProfileKey.isEmpty() || profile.profilePictureURL.isNullOrEmpty()) {
profileManager.setProfilePicture(context, recipient, null, null)
}
}
}
@@ -344,7 +385,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
message.threadID = threadID
val messageID =
storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID,
attachments, runIncrement, runThreadUpdate
attachments, runThreadUpdate
) ?: return null
// Parse & persist attachments
// Start attachment downloads if needed
@@ -447,12 +488,34 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
}
if (
message.kind !is ClosedGroupControlMessage.Kind.New &&
MessagingModuleConfiguration.shared.storage.canPerformConfigChange(
SharedConfigMessage.Kind.GROUPS.name,
MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!,
message.sentTimestamp!!
)
) {
// update the config
val closedGroupPublicKey = message.getPublicKey()
val storage = MessagingModuleConfiguration.shared.storage
storage.updateGroupConfig(closedGroupPublicKey)
}
}
private fun ClosedGroupControlMessage.getPublicKey(): String = kind!!.let { when (it) {
is ClosedGroupControlMessage.Kind.New -> it.publicKey.toByteArray().toHexString()
is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> it.publicKey?.toByteArray()?.toHexString() ?: groupPublicKey!!
is ClosedGroupControlMessage.Kind.MemberLeft -> groupPublicKey!!
is ClosedGroupControlMessage.Kind.MembersAdded -> groupPublicKey!!
is ClosedGroupControlMessage.Kind.MembersRemoved -> groupPublicKey!!
is ClosedGroupControlMessage.Kind.NameChange -> groupPublicKey!!
}}
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
if (!recipient.isApproved && !recipient.isLocalNumber) return
if (!recipient.isApproved && !recipient.isLocalNumber) return Log.e("Loki", "not accepting new closed group from unapproved recipient")
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
@@ -463,10 +526,24 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
// Create the group
val userPublicKey = storage.getUserPublicKey()!!
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val groupExists = storage.getGroup(groupID) != null
if (!storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, sentTimestamp)) {
// If the closed group already exists then store the encryption keys (since the config only stores
// the latest key we won't be able to decrypt older messages if we were added to the group within
// the last two weeks and the key has been rotated - unfortunately if the user was added more than
// two weeks ago and the keys were rotated within the last two weeks then we won't be able to decrypt
// messages received before the key rotation)
if (groupExists) {
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp)
storage.updateGroupConfig(groupPublicKey)
}
return
}
// Create the group
if (groupExists) {
// Update the group
if (!storage.isGroupActive(groupPublicKey)) {
@@ -485,11 +562,12 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp)
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), formationTimestamp, encryptionKeyPair)
// Set expiration timer
storage.setExpirationTimer(groupID, expireTimer)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
// Notify the user
if (userPublicKey == sender && !groupExists) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
@@ -537,7 +615,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
Log.d("Loki", "Ignoring duplicate closed group encryption key pair.")
return
}
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey, message.sentTimestamp!!)
Log.d("Loki", "Received a new closed group encryption key pair.")
}
@@ -565,7 +643,12 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.serialize() }
val name = kind.name
storage.updateTitle(groupID, name)
// Only update the group in storage if it isn't invalidated by the config state
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey!!, message.sentTimestamp!!)) {
storage.updateTitle(groupID, name)
}
// Notify the user
if (userPublicKey == senderPublicKey) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
@@ -599,12 +682,16 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Update zombie members in case the added members are zombies
val zombies = storage.getZombieMembers(groupID)
if (zombies.intersect(updateMembers).isNotEmpty()) {
storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
// Only update the group in storage if it isn't invalidated by the config state
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Update zombie members in case the added members are zombies
val zombies = storage.getZombieMembers(groupID)
if (zombies.intersect(updateMembers).isNotEmpty()) {
storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
}
}
// Notify the user
@@ -686,13 +773,18 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.")
}
val wasCurrentUserRemoved = userPublicKey in removedMembers
// Admin should send a MEMBERS_LEFT message but handled here just in case
if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else {
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Update zombie members
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
// Only update the group in storage if it isn't invalidated by the config state
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
// Admin should send a MEMBERS_LEFT message but handled here just in case
if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true)
return
} else {
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Update zombie members
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
}
}
// Notify the user
@@ -741,24 +833,30 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
val didAdminLeave = admins.contains(senderPublicKey)
val updatedMemberList = members - senderPublicKey
val userLeft = (userPublicKey == senderPublicKey)
if (didAdminLeave || userLeft) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else {
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
// Update zombie members
val zombies = storage.getZombieMembers(groupID)
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
// Only update the group in storage if it isn't invalidated by the config state
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
if (didAdminLeave || userLeft) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, delete = userLeft)
if (userLeft) {
return
}
} else {
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
// Update zombie members
val zombies = storage.getZombieMembers(groupID)
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
}
}
// Notify the user
if (userLeft) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
} else {
if (!userLeft) {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
}
}
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created
if (group.formationTimestamp > sentTimestamp) {
@@ -773,7 +871,7 @@ private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPu
return true
}
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) {
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean) {
val storage = MessagingModuleConfiguration.shared.storage
storage.removeClosedGroupPublicKey(groupPublicKey)
// Remove the key pairs
@@ -782,8 +880,14 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
storage.setActive(groupID, false)
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
// Stop polling
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
if (delete) {
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.cancelPendingMessageSendJobs(threadId)
storage.deleteConversation(threadId)
}
}
// endregion

View File

@@ -14,4 +14,4 @@ interface MessageNotifier {
fun updateNotification(context: Context, threadId: Long, signal: Boolean)
fun updateNotification(context: Context, signal: Boolean, reminderCount: Int)
fun clearReminder(context: Context)
}
}

View File

@@ -0,0 +1,126 @@
package org.session.libsession.messaging.sending_receiving.notifications
import com.goterl.lazysodium.utils.Key
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
* Changing the variable names will break how data is serialized/deserialized.
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
* single-letter keys to be as compact as possible.
*/
@Serializable
data class SubscriptionRequest(
/** the 33-byte account being subscribed to; typically a session ID */
val pubkey: String,
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
val session_ed25519: String?,
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
val subkey_tag: String? = null,
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
val namespaces: List<Int>,
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
val data: Boolean,
/** the signature unix timestamp in seconds, not ms */
val sig_ts: Long,
/** the 64-byte ed25519 signature */
val signature: String,
/** the string identifying the notification service, "firebase" for android (currently) */
val service: String,
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
val service_info: Map<String, String>,
/** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key.
* persist it on device */
val enc_key: String
)
@Serializable
data class UnsubscriptionRequest(
/** the 33-byte account being subscribed to; typically a session ID */
val pubkey: String,
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
val session_ed25519: String?,
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
val subkey_tag: String? = null,
/** the signature unix timestamp in seconds, not ms */
val sig_ts: Long,
/** the 64-byte ed25519 signature */
val signature: String,
/** the string identifying the notification service, "firebase" for android (currently) */
val service: String,
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
val service_info: Map<String, String>,
)
/** invalid values, missing reuqired arguments etc, details in message */
private const val UNPARSEABLE_ERROR = 1
/** the "service" value is not active / valid */
private const val SERVICE_NOT_AVAILABLE = 2
/** something getting wrong internally talking to the backend */
private const val SERVICE_TIMEOUT = 3
/** other error processing the subscription (details in the message) */
private const val GENERIC_ERROR = 4
@Serializable
data class SubscriptionResponse(
override val error: Int? = null,
override val message: String? = null,
override val success: Boolean? = null,
val added: Boolean? = null,
val updated: Boolean? = null,
): Response
@Serializable
data class UnsubscribeResponse(
override val error: Int? = null,
override val message: String? = null,
override val success: Boolean? = null,
val removed: Boolean? = null,
): Response
interface Response {
val error: Int?
val message: String?
val success: Boolean?
fun isSuccess() = success == true && error == null
fun isFailure() = !isSuccess()
}
@Serializable
data class PushNotificationMetadata(
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
@SerialName("@")
val account: String,
/** The hash of the message in the swarm. */
@SerialName("#")
val msg_hash: String,
/** The swarm namespace in which this message arrived. */
@SerialName("n")
val namespace: Int,
/** The length of the message data. This is always included, even if the message content
* itself was too large to fit into the push notification. */
@SerialName("l")
val data_len: Int,
/** This will be true if the data was omitted because it was too long to fit in a push
* notification (around 2.5kB of raw data), in which case the push notification includes
* only this metadata but not the message content itself. */
@SerialName("B")
val data_too_long : Boolean = false
)
@Serializable
data class PushNotificationServerObject(
val enc_payload: String,
val spns: Int,
) {
fun decryptPayload(key: Key): Any {
TODO()
}
}

View File

@@ -1,107 +0,0 @@
package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
@SuppressLint("StaticFieldLeak")
object PushNotificationAPI {
val context = MessagingModuleConfiguration.shared.context
val server = "https://live.apns.getsession.org"
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private val maxRetryCount = 4
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
enum class ClosedGroupOperation {
Subscribe, Unsubscribe;
val rawValue: String
get() {
return when (this) {
Subscribe -> "subscribe_closed_group"
Unsubscribe -> "unsubscribe_closed_group"
}
}
}
fun unregister(token: String) {
val parameters = mapOf( "token" to token )
val url = "$server/unregister"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
} else {
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
}
}
// Unsubscribe from all closed groups
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
}
}
fun register(token: String, publicKey: String, force: Boolean) {
val oldToken = TextSecurePreferences.getFCMToken(context)
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
val url = "$server/register"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setFCMToken(context, token)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
} else {
Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
}
}
// Subscribe to all closed groups
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
}
}
fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
if (!TextSecurePreferences.isUsingFCM(context)) { return }
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
val url = "$server/${operation.rawValue}"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
}
}
}
}

View File

@@ -0,0 +1,141 @@
package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint
import nl.komponents.kovenant.Promise
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.OnionResponse
import org.session.libsession.snode.Version
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.emptyPromise
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.sideEffect
@SuppressLint("StaticFieldLeak")
object PushRegistryV1 {
private val TAG = PushRegistryV1::class.java.name
val context = MessagingModuleConfiguration.shared.context
private const val maxRetryCount = 4
private val server = Server.LEGACY
fun register(
device: Device,
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
token: String? = TextSecurePreferences.getPushToken(context),
publicKey: String? = TextSecurePreferences.getLocalNumber(context),
legacyGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
): Promise<*, Exception> = when {
isPushEnabled -> retryIfNeeded(maxRetryCount) {
Log.d(TAG, "register() called")
doRegister(token, publicKey, device, legacyGroupPublicKeys)
} fail { exception ->
Log.d(TAG, "Couldn't register for FCM due to error", exception)
}
else -> emptyPromise()
}
private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection<String>): Promise<*, Exception> {
Log.d(TAG, "doRegister() called")
token ?: return emptyPromise()
publicKey ?: return emptyPromise()
val parameters = mapOf(
"token" to token,
"pubKey" to publicKey,
"device" to device.value,
"legacyGroupPublicKeys" to legacyGroupPublicKeys
)
val url = "${server.url}/register_legacy_groups_only"
val body = RequestBody.create(
MediaType.get("application/json"),
JsonUtil.toJson(parameters)
)
val request = Request.Builder().url(url).post(body).build()
return sendOnionRequest(request) sideEffect { response ->
when (response.code) {
null, 0 -> throw Exception("error: ${response.message}.")
}
} success {
Log.d(TAG, "registerV1 success")
}
}
/**
* Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
*/
fun unregister(): Promise<*, Exception> {
Log.d(TAG, "unregisterV1 requested")
val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise()
return retryIfNeeded(maxRetryCount) {
val parameters = mapOf("token" to token)
val url = "${server.url}/unregister"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()
sendOnionRequest(request) success {
when (it.code) {
null, 0 -> Log.d(TAG, "error: ${it.message}.")
else -> Log.d(TAG, "unregisterV1 success")
}
}
}
}
// Legacy Closed Groups
fun subscribeGroup(
closedGroupPublicKey: String,
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
) = if (isPushEnabled) {
performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey)
} else emptyPromise()
fun unsubscribeGroup(
closedGroupPublicKey: String,
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
) = if (isPushEnabled) {
performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
} else emptyPromise()
private fun performGroupOperation(
operation: String,
closedGroupPublicKey: String,
publicKey: String
): Promise<*, Exception> {
val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
val url = "${server.url}/$operation"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()
return retryIfNeeded(maxRetryCount) {
sendOnionRequest(request) sideEffect {
when (it.code) {
0, null -> throw Exception(it.message)
}
}
}
}
private fun sendOnionRequest(request: Request): Promise<OnionResponse, Exception> = OnionRequestAPI.sendOnionRequest(
request,
server.url,
server.publicKey,
Version.V2
)
}

View File

@@ -0,0 +1,6 @@
package org.session.libsession.messaging.sending_receiving.notifications
enum class Server(val url: String, val publicKey: String) {
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
}

View File

@@ -54,10 +54,9 @@ class ClosedGroupPollerV2 {
setUpPolling(groupPublicKey)
}
fun stop() {
val storage = MessagingModuleConfiguration.shared.storage
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
allGroupPublicKeys.iterator().forEach { stopPolling(it) }
fun stopAll() {
futures.forEach { it.value.cancel(false) }
isPolling.forEach { isPolling[it.key] = false }
}
fun stopPolling(groupPublicKey: String) {
@@ -80,7 +79,12 @@ class ClosedGroupPollerV2 {
// reasonable fake time interval to use instead.
val storage = MessagingModuleConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val threadID = storage.getThreadId(groupID) ?: return
val threadID = storage.getThreadId(groupID)
if (threadID == null) {
Log.d("Loki", "Stopping group poller due to missing thread for closed group: $groupPublicKey.")
stopPolling(groupPublicKey)
return
}
val lastUpdated = storage.getLastUpdated(threadID)
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
val minPollInterval = Companion.minPollInterval

View File

@@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.jobs.OpenGroupDeleteJob
import org.session.libsession.messaging.jobs.TrimThreadJob
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.Endpoint
@@ -30,6 +31,7 @@ import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.successBackground
import java.util.UUID
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
@@ -39,15 +41,101 @@ class OpenGroupPoller(private val server: String, private val executorService: S
var isCaughtUp = false
var secondToLastJob: MessageReceiveJob? = null
private var future: ScheduledFuture<*>? = null
@Volatile private var runId: UUID = UUID.randomUUID()
companion object {
private const val pollInterval: Long = 4000L
const val maxInactivityPeriod = 14 * 24 * 60 * 60 * 1000
public fun handleRoomPollInfo(
server: String,
roomToken: String,
pollInfo: OpenGroupApi.RoomPollInfo,
createGroupIfMissingWithPublicKey: String? = null
) {
val storage = MessagingModuleConfiguration.shared.storage
val groupId = "$server.$roomToken"
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
// If we don't have an existing group and don't have a 'createGroupIfMissingWithPublicKey'
// value then don't process the poll info
val publicKey = existingOpenGroup?.publicKey ?: createGroupIfMissingWithPublicKey
val name = pollInfo.details?.name ?: existingOpenGroup?.name
val infoUpdates = pollInfo.details?.infoUpdates ?: existingOpenGroup?.infoUpdates
if (publicKey == null) return
val openGroup = OpenGroup(
server = server,
room = pollInfo.token,
name = name ?: "",
publicKey = publicKey,
imageId = (pollInfo.details?.imageId ?: existingOpenGroup?.imageId),
canWrite = pollInfo.write,
infoUpdates = infoUpdates ?: 0
)
// - Open Group changes
storage.updateOpenGroup(openGroup)
// - User Count
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
// - Moderators
pollInfo.details?.moderators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.MODERATOR)
})
}
pollInfo.details?.hiddenModerators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)
})
}
// - Admins
pollInfo.details?.admins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.ADMIN)
})
}
pollInfo.details?.hiddenAdmins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
})
}
// Update the group avatar
if (
(
pollInfo.details != null &&
pollInfo.details.imageId != null && (
pollInfo.details.imageId != existingOpenGroup?.imageId ||
!storage.hasDownloadedProfilePicture(dbGroupId)
) &&
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, pollInfo.details.imageId) == null
) || (
pollInfo.details == null &&
existingOpenGroup?.imageId != null &&
!storage.hasDownloadedProfilePicture(dbGroupId) &&
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, existingOpenGroup.imageId) == null
)
) {
JobQueue.shared.add(GroupAvatarDownloadJob(server, roomToken, openGroup.imageId))
}
else if (
pollInfo.details != null &&
pollInfo.details.imageId == null &&
existingOpenGroup?.imageId != null
) {
storage.removeProfilePicture(dbGroupId)
}
}
}
fun startIfNeeded() {
if (hasStarted) { return }
hasStarted = true
runId = UUID.randomUUID()
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
}
@@ -57,6 +145,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
}
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
val currentRunId = runId
val storage = MessagingModuleConfiguration.shared.storage
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
@@ -81,27 +170,36 @@ class OpenGroupPoller(private val server: String, private val executorService: S
is Endpoint.Outbox, is Endpoint.OutboxSince -> {
handleDirectMessages(server, true, response.body as List<OpenGroupApi.DirectMessage>)
}
else -> { /* We don't care about the result of any other calls (won't be polled for) */}
}
if (secondToLastJob == null && !isCaughtUp) {
isCaughtUp = true
}
}
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
// Only poll again if it's the same poller run
if (currentRunId == runId) {
future = executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
}
}.fail {
updateCapabilitiesIfNeeded(isPostCapabilitiesRetry, it)
updateCapabilitiesIfNeeded(isPostCapabilitiesRetry, currentRunId, it)
}.map { }
}
private fun updateCapabilitiesIfNeeded(isPostCapabilitiesRetry: Boolean, exception: Exception) {
private fun updateCapabilitiesIfNeeded(isPostCapabilitiesRetry: Boolean, currentRunId: UUID, exception: Exception) {
if (exception is OnionRequestAPI.HTTPRequestFailedBlindingRequiredException) {
if (!isPostCapabilitiesRetry) {
OpenGroupApi.getCapabilities(server).map {
handleCapabilities(server, it)
}
executorService?.schedule({ poll(isPostCapabilitiesRetry = true) }, pollInterval, TimeUnit.MILLISECONDS)
// Only poll again if it's the same poller run
if (currentRunId == runId) {
future = executorService?.schedule({ poll(isPostCapabilitiesRetry = true) }, pollInterval, TimeUnit.MILLISECONDS)
}
}
} else {
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
} else if (currentRunId == runId) {
future = executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
}
}
@@ -109,73 +207,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
val storage = MessagingModuleConfiguration.shared.storage
storage.setServerCapabilities(server, capabilities.capabilities)
}
private fun handleRoomPollInfo(
server: String,
roomToken: String,
pollInfo: OpenGroupApi.RoomPollInfo
) {
val storage = MessagingModuleConfiguration.shared.storage
val groupId = "$server.$roomToken"
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
val publicKey = existingOpenGroup?.publicKey ?: return
val openGroup = OpenGroup(
server = server,
room = pollInfo.token,
name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name },
publicKey = publicKey,
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId },
canWrite = pollInfo.write,
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates }
)
// - Open Group changes
storage.updateOpenGroup(openGroup)
// - User Count
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
// - Moderators
pollInfo.details?.moderators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.MODERATOR)
})
}
pollInfo.details?.hiddenModerators?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)
})
}
// - Admins
pollInfo.details?.admins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.ADMIN)
})
}
pollInfo.details?.hiddenAdmins?.let { moderatorList ->
storage.setGroupMemberRoles(moderatorList.map {
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
})
}
if (
(
pollInfo.details != null &&
pollInfo.details.imageId != null && (
pollInfo.details.imageId != existingOpenGroup.imageId ||
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) || (
pollInfo.details == null &&
existingOpenGroup.imageId != null &&
!storage.hasDownloadedProfilePicture(dbGroupId)
)
) {
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
}
}
private fun handleMessages(
server: String,
roomToken: String,
@@ -230,7 +262,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S
null,
fromOutbox,
if (fromOutbox) it.recipient else it.sender,
serverPublicKey
serverPublicKey,
emptySet() // this shouldn't be necessary as we are polling open groups here
)
if (fromOutbox) {
val mapping = mappingCache[it.recipient] ?: storage.getOrCreateBlindedIdMapping(
@@ -247,7 +280,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S
}
mappingCache[it.recipient] = mapping
}
MessageReceiver.handle(message, proto, null)
val threadId = Message.getThreadId(message, null, MessagingModuleConfiguration.shared.storage, false)
MessageReceiver.handle(message, proto, threadId ?: -1, null)
} catch (e: Exception) {
Log.e("Loki", "Couldn't handle direct message", e)
}

View File

@@ -1,5 +1,14 @@
package org.session.libsession.messaging.sending_receiving.pollers
import android.util.SparseArray
import androidx.core.util.valueIterator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
@@ -10,17 +19,23 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.snode.RawResponse
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeModule
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import java.security.SecureRandom
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
private class PromiseCanceledException : Exception("Promise canceled.")
class Poller {
class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Timer) {
var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
private var hasStarted: Boolean = false
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
@@ -97,23 +112,159 @@ class Poller {
}
}
private fun processPersonalMessages(snode: Snode, rawMessages: RawResponse) {
val messages = SnodeAPI.parseRawMessagesResponse(rawMessages, snode, userPublicKey)
val parameters = messages.map { (envelope, serverHash) ->
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
}
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk ->
val job = BatchMessageReceiveJob(chunk)
JobQueue.shared.add(job)
}
}
private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) {
if (forConfigObject == null) return
val messages = SnodeAPI.parseRawMessagesResponse(
rawMessages,
snode,
userPublicKey,
namespace,
updateLatestHash = true,
updateStoredHashes = true,
)
if (messages.isEmpty()) {
// no new messages to process
return
}
var latestMessageTimestamp: Long? = null
messages.forEach { (envelope, hash) ->
try {
val (message, _) = MessageReceiver.parse(data = envelope.toByteArray(),
// assume no groups in personal poller messages
openGroupServerID = null, currentClosedGroups = emptySet()
)
// sanity checks
if (message !is SharedConfigurationMessage) {
Log.w("Loki", "shared config message handled in configs wasn't SharedConfigurationMessage but was ${message.javaClass.simpleName}")
return@forEach
}
forConfigObject.merge(hash!! to message.data)
latestMessageTimestamp = if ((message.sentTimestamp ?: 0L) > (latestMessageTimestamp ?: 0L)) { message.sentTimestamp } else { latestMessageTimestamp }
} catch (e: Exception) {
Log.e("Loki", e)
}
}
// process new results
if (forConfigObject.needsDump()) {
configFactory.persist(forConfigObject, latestMessageTimestamp ?: SnodeAPI.nowWithOffset)
}
}
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
return SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse ->
isCaughtUp = true
if (deferred.promise.isDone()) {
task { Unit } // The long polling connection has been canceled; don't recurse
} else {
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
val parameters = messages.map { (envelope, serverHash) ->
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
return task {
runBlocking(Dispatchers.IO) {
val requestSparseArray = SparseArray<SnodeAPI.SnodeBatchRequestInfo>()
// get messages
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(snode, userPublicKey, maxSize = -2)!!.also { personalMessages ->
// namespaces here should always be set
requestSparseArray[personalMessages.namespace!!] = personalMessages
}
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk ->
val job = BatchMessageReceiveJob(chunk)
JobQueue.shared.add(job)
// get the latest convo info volatile
val hashesToExtend = mutableSetOf<String>()
configFactory.getUserConfigs().mapNotNull { config ->
hashesToExtend += config.currentHashes()
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode, userPublicKey,
config.configNamespace(),
maxSize = -8
)
}.forEach { request ->
// namespaces here should always be set
requestSparseArray[request.namespace!!] = request
}
poll(snode, deferred)
val requests =
requestSparseArray.valueIterator().asSequence().toMutableList()
if (hashesToExtend.isNotEmpty()) {
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest(
messageHashes = hashesToExtend.toList(),
publicKey = userPublicKey,
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
extend = true
)?.let { extensionRequest ->
requests += extensionRequest
}
}
SnodeAPI.getRawBatchResponse(snode, userPublicKey, requests).bind { rawResponses ->
isCaughtUp = true
if (deferred.promise.isDone()) {
return@bind Promise.ofSuccess(Unit)
} else {
val responseList = (rawResponses["results"] as List<RawResponse>)
// in case we had null configs, the array won't be fully populated
// index of the sparse array key iterator should be the request index, with the key being the namespace
// TODO: add in specific ordering of config namespaces for processing
listOfNotNull(
configFactory.user?.configNamespace(),
configFactory.contacts?.configNamespace(),
configFactory.userGroups?.configNamespace(),
configFactory.convoVolatile?.configNamespace()
).map {
it to requestSparseArray.indexOfKey(it)
}.filter { (_, i) -> i >= 0 }.forEach { (key, requestIndex) ->
responseList.getOrNull(requestIndex)?.let { rawResponse ->
if (rawResponse["code"] as? Int != 200) {
Log.e("Loki", "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
return@forEach
}
val body = rawResponse["body"] as? RawResponse
if (body == null) {
Log.e("Loki", "Batch sub-request didn't contain a body")
return@forEach
}
if (key == Namespace.DEFAULT) {
return@forEach // continue, skip default namespace
} else {
when (ConfigBase.kindFor(key)) {
UserProfile::class.java -> processConfig(snode, body, key, configFactory.user)
Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts)
ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile)
UserGroupsConfig::class.java -> processConfig(snode, body, key, configFactory.userGroups)
}
}
}
}
// the first response will be the personal messages (we want these to be processed after config messages)
val personalResponseIndex = requestSparseArray.indexOfKey(Namespace.DEFAULT)
if (personalResponseIndex >= 0) {
responseList.getOrNull(personalResponseIndex)?.let { rawResponse ->
if (rawResponse["code"] as? Int != 200) {
Log.e("Loki", "Batch sub-request for personal messages had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
} else {
val body = rawResponse["body"] as? RawResponse
if (body == null) {
Log.e("Loki", "Batch sub-request for personal messages didn't contain a body")
} else {
processPersonalMessages(snode, body)
}
}
}
}
poll(snode, deferred)
}
}.fail {
Log.e("Loki", "Failed to get raw batch response", it)
poll(snode, deferred)
}
}
}
}

View File

@@ -205,7 +205,7 @@ object SodiumUtilities {
}
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
val plaintext = ByteArray(plaintextSize)
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
plaintext,

View File

@@ -4,51 +4,51 @@ import android.content.Context
import org.session.libsession.R
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED
import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.truncateIdForDisplay
object UpdateMessageBuilder {
val storage = MessagingModuleConfiguration.shared.storage
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
var message = ""
val updateData = updateMessageData.kind ?: return message
if (!isOutgoing && sender == null) return message
val storage = MessagingModuleConfiguration.shared.storage
val senderName: String = if (!isOutgoing) {
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
} else { context.getString(R.string.MessageRecord_you) }
fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId)
?.displayName(Contact.ContactContext.REGULAR)
?: truncateIdForDisplay(senderId)
when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_you_created_a_new_group)
} else {
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
}
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
val updateData = updateMessageData.kind
if (updateData == null || !isOutgoing && senderId == null) return ""
val senderName: String = if (isOutgoing) context.getString(R.string.MessageRecord_you)
else getSenderName(senderId!!)
return when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) {
context.getString(R.string.MessageRecord_you_created_a_new_group)
} else {
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
}
is UpdateMessageData.Kind.GroupNameChange -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
} else {
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
}
is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) {
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
} else {
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
}
is UpdateMessageData.Kind.GroupMemberAdded -> {
val members = updateData.updatedMembers.joinToString(", ") {
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
}
message = if (isOutgoing) {
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) {
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
} else {
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
}
}
is UpdateMessageData.Kind.GroupMemberRemoved -> {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
// 1st case: you are part of the removed members
message = if (userPublicKey in updateData.updatedMembers) {
return if (userPublicKey in updateData.updatedMembers) {
if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
@@ -56,9 +56,7 @@ object UpdateMessageBuilder {
}
} else {
// 2nd case: you are not part of the removed members
val members = updateData.updatedMembers.joinToString(", ") {
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
}
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) {
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
} else {
@@ -66,22 +64,19 @@ object UpdateMessageBuilder {
}
}
}
is UpdateMessageData.Kind.GroupMemberLeft -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
context.getString(R.string.ConversationItem_group_action_left, senderName)
}
is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
context.getString(R.string.ConversationItem_group_action_left, senderName)
}
else -> return ""
}
return message
}
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String {
if (!isOutgoing && sender == null) return ""
val storage = MessagingModuleConfiguration.shared.storage
val senderName: String? = if (!isOutgoing) {
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
if (!isOutgoing && senderId == null) return ""
val senderName: String = if (!isOutgoing) {
getSenderName(senderId!!)
} else { context.getString(R.string.MessageRecord_you) }
return if (duration <= 0) {
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
@@ -93,9 +88,8 @@ object UpdateMessageBuilder {
}
}
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String {
val storage = MessagingModuleConfiguration.shared.storage
val senderName = storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender!!
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
val senderName = getSenderName(senderId!!)
return when (kind) {
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
@@ -104,18 +98,12 @@ object UpdateMessageBuilder {
}
}
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String {
val storage = MessagingModuleConfiguration.shared.storage
val senderName = storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender
return when (type) {
CallMessageType.CALL_MISSED ->
context.getString(R.string.MessageRecord_missed_call_from, senderName)
CallMessageType.CALL_INCOMING ->
context.getString(R.string.MessageRecord_s_called_you, senderName)
CallMessageType.CALL_OUTGOING ->
context.getString(R.string.MessageRecord_called_s, senderName)
CallMessageType.CALL_FIRST_MISSED ->
context.getString(R.string.MessageRecord_missed_call_from, senderName)
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
when (type) {
CALL_INCOMING -> R.string.MessageRecord_s_called_you
CALL_OUTGOING -> R.string.MessageRecord_called_s
CALL_MISSED, CALL_FIRST_MISSED -> R.string.MessageRecord_missed_call_from
}.let {
context.getString(it, storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender)
}
}
}

View File

@@ -26,6 +26,7 @@ import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.recover
import org.session.libsignal.utilities.toHexString
import java.util.Date
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.set
private typealias Path = List<Snode>
@@ -43,13 +44,27 @@ object OnionRequestAPI {
private val snodeFailureCount = mutableMapOf<Snode, Int>()
var guardSnodes = setOf<Snode>()
var _paths: AtomicReference<List<Path>?> = AtomicReference(null)
var paths: List<Path> // Not a set to ensure we consistently show the same path to the user
get() = database.getOnionRequestPaths()
get() {
val paths = _paths.get()
if (paths != null) { return paths }
// Storing this in an atomic variable as it was causing a number of background
// ANRs when this value was accessed via the main thread after tapping on
// a notification)
val result = database.getOnionRequestPaths()
_paths.set(result)
return result
}
set(newValue) {
if (newValue.isEmpty()) {
database.clearOnionRequestPaths()
_paths.set(null)
} else {
database.setOnionRequestPaths(newValue)
_paths.set(newValue)
}
}
@@ -404,6 +419,8 @@ object OnionRequestAPI {
Log.d("Loki","Destination server returned ${exception.statusCode}")
} else if (message == "Loki Server error") {
Log.d("Loki", "message was $message")
} else if (exception.statusCode == 404) {
// 404 is probably file server missing a file, don't rebuild path or mark a snode as bad here
} else { // Only drop snode/path if not receiving above two exception cases
handleUnspecificError()
}
@@ -431,8 +448,8 @@ object OnionRequestAPI {
val payloadData = JsonUtil.toJson(payload).toByteArray()
return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception ->
val error = when (exception) {
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
else -> null
}
if (error != null) { throw error }
@@ -669,4 +686,7 @@ enum class Version(val value: String) {
data class OnionResponse(
val info: Map<*, *>,
val body: ByteArray? = null
)
) {
val code: Int? get() = info["code"] as? Int
val message: String? get() = info["message"] as? String
}

View File

@@ -28,12 +28,12 @@ import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.prettifiedDescription
import org.session.libsignal.utilities.retryIfNeeded
import java.security.SecureRandom
import java.util.Date
import java.util.Locale
import kotlin.collections.component1
import kotlin.collections.component2
@@ -57,7 +57,8 @@ object SnodeAPI {
*/
internal var clockOffset = 0L
val nowWithOffset
@JvmStatic
public val nowWithOffset
get() = System.currentTimeMillis() + clockOffset
internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue ->
@@ -72,12 +73,16 @@ object SnodeAPI {
private val minimumSnodePoolCount = 12
private val minimumSwarmSnodeCount = 3
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443
private val seedNodePool by lazy {
if (useTestnet) {
setOf( "http://public.loki.foundation:38157" )
} else {
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
setOf(
"https://seed1.getsession.org:$seedNodePort",
"https://seed2.getsession.org:$seedNodePort",
"https://seed3.getsession.org:$seedNodePort",
)
}
}
private const val snodeFailureThreshold = 3
@@ -97,6 +102,14 @@ object SnodeAPI {
object ValidationFailed : Error("ONS name validation failed.")
}
// Batch
data class SnodeBatchRequestInfo(
val method: String,
val params: Map<String, Any>,
@Transient
val namespace: Int?
) // assume signatures, pubkey and namespaces are attached in parameters if required
// Internal API
internal fun invoke(
method: Snode.Method,
@@ -314,26 +327,32 @@ object SnodeAPI {
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
// Get last message hash
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
val parameters = mutableMapOf<String,Any>(
val parameters = mutableMapOf<String, Any>(
"pubKey" to publicKey,
"last_hash" to lastHashValue,
)
// Construct signature
if (requiresAuth) {
val userED25519KeyPair = try {
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair)
MessagingModuleConfiguration.shared.getUserED25519KeyPair()
?: return Promise.ofFail(Error.NoKeyPair)
} catch (e: Exception) {
Log.e("Loki", "Error getting KeyPair", e)
return Promise.ofFail(Error.NoKeyPair)
}
val timestamp = Date().time + SnodeAPI.clockOffset
val timestamp = System.currentTimeMillis() + clockOffset
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
val verificationData =
if (namespace != 0) "retrieve$namespace$timestamp".toByteArray()
else "retrieve$timestamp".toByteArray()
try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userED25519KeyPair.secretKey.asBytes
)
} catch (exception: Exception) {
return Promise.ofFail(Error.SigningFailed)
}
@@ -349,7 +368,251 @@ object SnodeAPI {
}
// Make the request
return invoke(Snode.Method.GetMessages, snode, parameters, publicKey)
return invoke(Snode.Method.Retrieve, snode, parameters, publicKey)
}
fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? {
val params = mutableMapOf<String, Any>()
// load the message data params into the sub request
// currently loads:
// pubKey
// data
// ttl
// timestamp
params.putAll(message.toJSON())
params["namespace"] = namespace
// used for sig generation since it is also the value used in timestamp parameter
val messageTimestamp = message.timestamp
val userEd25519KeyPair = try {
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
} catch (e: Exception) {
return null
}
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
val verificationData = "store$namespace$messageTimestamp".toByteArray()
try {
sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
}
// timestamp already set
params["pubkey_ed25519"] = ed25519PublicKey
params["signature"] = Base64.encodeBytes(signature)
return SnodeBatchRequestInfo(
Snode.Method.SendMessage.rawValue,
params,
namespace
)
}
/**
* Message hashes can be shared across multiple namespaces (for a single public key destination)
* @param publicKey the destination's identity public key to delete from (05...)
* @param messageHashes a list of stored message hashes to delete from the server
* @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404
*/
fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List<String>, required: Boolean = false): SnodeBatchRequestInfo? {
val params = mutableMapOf(
"pubkey" to publicKey,
"required" to required, // could be omitted technically but explicit here
"messages" to messageHashes
)
val userEd25519KeyPair = try {
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
} catch (e: Exception) {
return null
}
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
val verificationData = "delete${messageHashes.joinToString("")}".toByteArray()
try {
sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
return null
}
params["pubkey_ed25519"] = ed25519PublicKey
params["signature"] = Base64.encodeBytes(signature)
return SnodeBatchRequestInfo(
Snode.Method.DeleteMessage.rawValue,
params,
null
)
}
fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0, maxSize: Int? = null): SnodeBatchRequestInfo? {
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
val params = mutableMapOf<String, Any>(
"pubkey" to publicKey,
"last_hash" to lastHashValue,
)
val userEd25519KeyPair = try {
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
} catch (e: Exception) {
return null
}
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val timestamp = System.currentTimeMillis() + clockOffset
val signature = ByteArray(Sign.BYTES)
val verificationData = if (namespace == 0) "retrieve$timestamp".toByteArray()
else "retrieve$namespace$timestamp".toByteArray()
try {
sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
return null
}
params["timestamp"] = timestamp
params["pubkey_ed25519"] = ed25519PublicKey
params["signature"] = Base64.encodeBytes(signature)
if (namespace != 0) {
params["namespace"] = namespace
}
if (maxSize != null) {
params["max_size"] = maxSize
}
return SnodeBatchRequestInfo(
Snode.Method.Retrieve.rawValue,
params,
namespace
)
}
fun buildAuthenticatedAlterTtlBatchRequest(
messageHashes: List<String>,
newExpiry: Long,
publicKey: String,
shorten: Boolean = false,
extend: Boolean = false): SnodeBatchRequestInfo? {
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) ?: return null
return SnodeBatchRequestInfo(
Snode.Method.Expire.rawValue,
params,
null
)
}
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
val parameters = mutableMapOf<String, Any>(
"requests" to requests
)
return invoke(if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode, parameters, publicKey).success { rawResponses ->
val responseList = (rawResponses["results"] as List<RawResponse>)
responseList.forEachIndexed { index, response ->
if (response["code"] as? Int != 200) {
Log.w("Loki", "response code was not 200")
handleSnodeError(
response["code"] as? Int ?: 0,
response,
snode,
publicKey
)
}
}
}
}
fun getExpiries(messageHashes: List<String>, publicKey: String) : RawResponsePromise {
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(NullPointerException("No user key pair"))
return retryIfNeeded(maxRetryCount) {
val timestamp = System.currentTimeMillis() + clockOffset
val params = mutableMapOf(
"pubkey" to publicKey,
"messages" to messageHashes,
"timestamp" to timestamp
)
val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${messageHashes.joinToString(separator = "")}".toByteArray()
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(
signature,
signData,
signData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
return@retryIfNeeded Promise.ofFail(e)
}
params["pubkey_ed25519"] = ed25519PublicKey
params["signature"] = Base64.encodeBytes(signature)
getSingleTargetSnode(publicKey).bind { snode ->
invoke(Snode.Method.GetExpiries, snode, params, publicKey)
}
}
}
fun alterTtl(messageHashes: List<String>, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise {
return retryIfNeeded(maxRetryCount) {
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten)
?: return@retryIfNeeded Promise.ofFail(
Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten")
)
getSingleTargetSnode(publicKey).bind { snode ->
invoke(Snode.Method.Expire, snode, params, publicKey)
}
}
}
private fun buildAlterTtlParams( // TODO: in future this will probably need to use the closed group subkeys / admin keys for group swarms
messageHashes: List<String>,
newExpiry: Long,
publicKey: String,
extend: Boolean = false,
shorten: Boolean = false): Map<String, Any>? {
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
val params = mutableMapOf(
"expiry" to newExpiry,
"messages" to messageHashes,
)
if (extend) {
params["extend"] = true
} else if (shorten) {
params["shorten"] = true
}
val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else ""
val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray()
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(
signature,
signData,
signData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
return null
}
params["pubkey"] = publicKey
params["pubkey_ed25519"] = ed25519PublicKey
params["signature"] = Base64.encodeBytes(signature)
return params
}
fun getMessages(publicKey: String): MessageListPromise {
@@ -375,7 +638,7 @@ object SnodeAPI {
val parameters = message.toJSON().toMutableMap<String,Any>()
// Construct signature
if (requiresAuth) {
val sigTimestamp = System.currentTimeMillis() + SnodeAPI.clockOffset
val sigTimestamp = nowWithOffset
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
// assume namespace here is non-zero, as zero namespace doesn't require auth
@@ -478,13 +741,14 @@ object SnodeAPI {
retryIfNeeded(maxRetryCount) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val verificationData = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
val deleteMessageParams = mapOf(
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
"signature" to Base64.encodeBytes(signature),
"namespace" to Namespace.ALL,
)
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map {
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
@@ -497,11 +761,13 @@ object SnodeAPI {
}
}
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> {
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> {
val messages = rawResponse["messages"] as? List<*>
return if (messages != null) {
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
val newRawMessages = removeDuplicates(publicKey, messages, namespace)
if (updateLatestHash) {
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
}
val newRawMessages = removeDuplicates(publicKey, messages, namespace, updateStoredHashes)
return parseEnvelopes(newRawMessages)
} else {
listOf()
@@ -518,7 +784,7 @@ object SnodeAPI {
}
}
private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int): List<*> {
private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> {
val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf()
val receivedMessageHashValues = originalMessageHashValues.toMutableSet()
val result = rawMessages.filter { rawMessage ->
@@ -533,7 +799,7 @@ object SnodeAPI {
false
}
}
if (originalMessageHashValues != receivedMessageHashValues) {
if (originalMessageHashValues != receivedMessageHashValues && updateStoredHashes) {
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace)
}
return result
@@ -570,11 +836,11 @@ object SnodeAPI {
Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
false
} else {
val hashes = json["deleted"] as List<String> // Hashes of deleted messages
val hashes = (json["deleted"] as Map<String,List<String>>).flatMap { (_, hashes) -> hashes }.sorted() // Hashes of deleted messages
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
val message = (userPublicKey + timestamp.toString() + hashes.fold("") { a, v -> a + v }).toByteArray()
val message = (userPublicKey + timestamp.toString() + hashes.joinToString(separator = "")).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
}
}
@@ -630,6 +896,10 @@ object SnodeAPI {
Log.d("Loki", "Got a 421 without an associated public key.")
}
}
404 -> {
Log.d("Loki", "404, probably no file found")
return Error.Generic
}
else -> {
handleBadSnode()
Log.d("Loki", "Unhandled response code: ${statusCode}.")

View File

@@ -20,6 +20,7 @@ data class SnodeMessage(
*/
val timestamp: Long
) {
internal constructor(): this("", "", -1, -1)
internal fun toJSON(): Map<String, String> {
return mapOf(

View File

@@ -1,6 +1,7 @@
package org.session.libsession.utilities
import androidx.annotation.WorkerThread
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Util
import org.session.libsignal.utilities.Hex
@@ -27,9 +28,11 @@ internal object AESGCM {
internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = ivAndCiphertext.sliceArray(0 until ivSize)
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count())
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return cipher.doFinal(ciphertext)
synchronized(CIPHER_LOCK) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return cipher.doFinal(ciphertext)
}
}
/**
@@ -47,9 +50,11 @@ internal object AESGCM {
*/
internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray {
val iv = Util.getSecretBytes(ivSize)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
synchronized(CIPHER_LOCK) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
}
}
/**

View File

@@ -5,11 +5,11 @@ import android.os.Parcel
import android.os.Parcelable
import android.util.Pair
import androidx.annotation.VisibleForTesting
import org.session.libsession.utilities.DelimiterUtil
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Util
import java.util.*
import org.session.libsignal.utilities.guava.Optional
import java.util.Collections
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicReference
import java.util.regex.Matcher
import java.util.regex.Pattern
@@ -27,6 +27,8 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
get() = GroupUtil.isOpenGroup(address)
val isOpenGroupInbox: Boolean
get() = GroupUtil.isOpenGroupInbox(address)
val isOpenGroupOutbox: Boolean
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
val isContact: Boolean
get() = !(isGroup || isOpenGroupInbox)

View File

@@ -0,0 +1,23 @@
package org.session.libsession.utilities
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
interface ConfigFactoryProtocol {
val user: UserProfile?
val contacts: Contacts?
val convoVolatile: ConversationVolatileConfig?
val userGroups: UserGroupsConfig?
fun getUserConfigs(): List<ConfigBase>
fun persist(forConfigObject: ConfigBase, timestamp: Long)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
}
interface ConfigFactoryUpdateListener {
fun notifyUpdates(forConfigObject: ConfigBase)
}

View File

@@ -0,0 +1,6 @@
package org.session.libsession.utilities
enum class Device(val value: String, val service: String = value) {
ANDROID("android", "firebase"),
HUAWEI("huawei");
}

View File

@@ -4,7 +4,9 @@ import okhttp3.HttpUrl
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
import java.io.*
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
object DownloadUtilities {
@@ -14,7 +16,7 @@ object DownloadUtilities {
@JvmStatic
fun downloadFile(destination: File, url: String) {
val outputStream = FileOutputStream(destination) // Throws
var remainingAttempts = 4
var remainingAttempts = 2
var exception: Exception? = null
while (remainingAttempts > 0) {
remainingAttempts -= 1

View File

@@ -1,9 +1,9 @@
package org.session.libsession.utilities
import network.loki.messenger.libsession_util.util.GroupInfo
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex
import java.io.IOException
import kotlin.jvm.Throws
object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
@@ -97,4 +97,28 @@ object GroupUtil {
fun doubleDecodeGroupID(groupID: String): ByteArray {
return getDecodedGroupIDAsData(getDecodedGroupID(groupID))
}
@JvmStatic
@Throws(IOException::class)
fun doubleDecodeGroupId(groupID: String): String {
return Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID)))
}
fun createConfigMemberMap(
members: Collection<String>,
admins: Collection<String>
): Map<String, Boolean> {
// Start with admins
val memberMap = admins.associate {
it to true
}.toMutableMap()
// Add the remaining members (there may be duplicates, so only add ones that aren't already in there from admins)
for (member in members) {
if (!memberMap.contains(member)) {
memberMap[member] = false
}
}
return memberMap
}
}

View File

@@ -0,0 +1,4 @@
package org.session.libsession.utilities
fun truncateIdForDisplay(id: String): String =
id.takeIf { it.length > 8 }?.apply{ "${take(4)}${takeLast(4)}" } ?: id

View File

@@ -1,23 +1,24 @@
package org.session.libsession.utilities;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsignal.utilities.Base64;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import java.io.IOException;
public class ProfileKeyUtil {
public static final int PROFILE_KEY_BYTES = 32;
public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
try {
String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
if (encodedProfileKey == null) {
encodedProfileKey = Util.getSecret(32);
encodedProfileKey = Util.getSecret(PROFILE_KEY_BYTES);
TextSecurePreferences.setProfileKey(context, encodedProfileKey);
}
@@ -36,7 +37,7 @@ public class ProfileKeyUtil {
}
public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) {
return Util.getSecret(32);
return Util.getSecret(PROFILE_KEY_BYTES);
}
public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) {

View File

@@ -1,9 +1,9 @@
package org.session.libsession.utilities
import android.content.Context
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
class SSKEnvironment(
@@ -26,14 +26,14 @@ class SSKEnvironment(
interface ProfileManagerProtocol {
companion object {
const val NAME_PADDED_LENGTH = 26
const val NAME_PADDED_LENGTH = 64
}
fun setNickname(context: Context, recipient: Recipient, nickname: String?)
fun setName(context: Context, recipient: Recipient, name: String)
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
fun setName(context: Context, recipient: Recipient, name: String?)
fun setProfilePicture(context: Context, recipient: Recipient, profilePictureURL: String?, profileKey: ByteArray?)
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
fun contactUpdatedInternal(contact: Contact): String?
}
interface MessageExpirationManagerProtocol {
@@ -54,4 +54,4 @@ class SSKEnvironment(
shared = SSKEnvironment(typingIndicators, readReceiptManager, profileManager, notificationManager, messageExpirationManager)
}
}
}
}

View File

@@ -12,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.BuildConfig
import org.session.libsession.R
import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
@@ -38,12 +37,12 @@ interface TextSecurePreferences {
fun setLastConfigurationSyncTime(value: Long)
fun getConfigurationMessageSynced(): Boolean
fun setConfigurationMessageSynced(value: Boolean)
fun isUsingFCM(): Boolean
fun setIsUsingFCM(value: Boolean)
fun getFCMToken(): String?
fun setFCMToken(value: String)
fun getLastFCMUploadTime(): Long
fun setLastFCMUploadTime(value: Long)
fun isPushEnabled(): Boolean
fun setPushEnabled(value: Boolean)
fun getPushToken(): String?
fun setPushToken(value: String)
fun getPushRegisterTime(): Long
fun setPushRegisterTime(value: Long)
fun isScreenLockEnabled(): Boolean
fun setScreenLockEnabled(value: Boolean)
fun getScreenLockTimeout(): Long
@@ -103,6 +102,8 @@ interface TextSecurePreferences {
fun setUpdateApkDigest(value: String?)
fun getUpdateApkDigest(): String?
fun getLocalNumber(): String?
fun getHasLegacyConfig(): Boolean
fun setHasLegacyConfig(newValue: Boolean)
fun setLocalNumber(localNumber: String)
fun removeLocalNumber()
fun isEnterSendsEnabled(): Boolean
@@ -178,6 +179,7 @@ interface TextSecurePreferences {
fun setThemeStyle(themeStyle: String)
fun setFollowSystemSettings(followSystemSettings: Boolean)
fun autoplayAudioMessages(): Boolean
fun hasForcedNewConfig(): Boolean
fun hasPreference(key: String): Boolean
fun clearAll()
@@ -249,9 +251,9 @@ interface TextSecurePreferences {
const val LINK_PREVIEWS = "pref_link_previews"
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
const val IS_USING_FCM = "pref_is_using_fcm"
const val FCM_TOKEN = "pref_fcm_token"
const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
const val IS_PUSH_ENABLED = "pref_is_using_fcm"
const val PUSH_TOKEN = "pref_fcm_token_2"
const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2"
const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
@@ -264,6 +266,10 @@ interface TextSecurePreferences {
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color"
const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
const val GREEN_ACCENT = "accent_green"
const val BLUE_ACCENT = "accent_blue"
const val PURPLE_ACCENT = "accent_purple"
@@ -303,31 +309,31 @@ interface TextSecurePreferences {
}
@JvmStatic
fun isUsingFCM(context: Context): Boolean {
return getBooleanPreference(context, IS_USING_FCM, false)
fun isPushEnabled(context: Context): Boolean {
return getBooleanPreference(context, IS_PUSH_ENABLED, false)
}
@JvmStatic
fun setIsUsingFCM(context: Context, value: Boolean) {
setBooleanPreference(context, IS_USING_FCM, value)
fun setPushEnabled(context: Context, value: Boolean) {
setBooleanPreference(context, IS_PUSH_ENABLED, value)
}
@JvmStatic
fun getFCMToken(context: Context): String? {
return getStringPreference(context, FCM_TOKEN, "")
fun getPushToken(context: Context): String? {
return getStringPreference(context, PUSH_TOKEN, "")
}
@JvmStatic
fun setFCMToken(context: Context, value: String) {
setStringPreference(context, FCM_TOKEN, value)
fun setPushToken(context: Context, value: String?) {
setStringPreference(context, PUSH_TOKEN, value)
}
fun getLastFCMUploadTime(context: Context): Long {
return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0)
fun getPushRegisterTime(context: Context): Long {
return getLongPreference(context, PUSH_REGISTER_TIME, 0)
}
fun setLastFCMUploadTime(context: Context, value: Long) {
setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value)
fun setPushRegisterTime(context: Context, value: Long) {
setLongPreference(context, PUSH_REGISTER_TIME, value)
}
// endregion
@@ -625,6 +631,17 @@ interface TextSecurePreferences {
return getStringPreference(context, LOCAL_NUMBER_PREF, null)
}
@JvmStatic
fun getHasLegacyConfig(context: Context): Boolean {
return getBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, false)
}
@JvmStatic
fun setHasLegacyConfig(context: Context, newValue: Boolean) {
setBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, newValue)
_events.tryEmit(HAS_RECEIVED_LEGACY_CONFIG)
}
fun setLocalNumber(context: Context, localNumber: String) {
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase())
}
@@ -649,7 +666,7 @@ interface TextSecurePreferences {
@JvmStatic
fun isScreenSecurityEnabled(context: Context): Boolean {
return getBooleanPreference(context, SCREEN_SECURITY_PREF, !BuildConfig.DEBUG)
return getBooleanPreference(context, SCREEN_SECURITY_PREF, context.resources.getBoolean(R.bool.screen_security_default))
}
fun getLastVersionCode(context: Context): Int {
@@ -795,6 +812,11 @@ interface TextSecurePreferences {
setIntegerPreference(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
}
@JvmStatic
fun hasForcedNewConfig(context: Context): Boolean {
return getBooleanPreference(context, HAS_FORCED_NEW_CONFIG, false)
}
@JvmStatic
fun getBooleanPreference(context: Context, key: String?, defaultValue: Boolean): Boolean {
return getDefaultSharedPreferences(context).getBoolean(key, defaultValue)
@@ -986,7 +1008,6 @@ interface TextSecurePreferences {
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
}
}
}
@@ -1011,28 +1032,28 @@ class AppTextSecurePreferences @Inject constructor(
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
}
override fun isUsingFCM(): Boolean {
return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false)
override fun isPushEnabled(): Boolean {
return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false)
}
override fun setIsUsingFCM(value: Boolean) {
setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value)
override fun setPushEnabled(value: Boolean) {
setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value)
}
override fun getFCMToken(): String? {
return getStringPreference(TextSecurePreferences.FCM_TOKEN, "")
override fun getPushToken(): String? {
return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "")
}
override fun setFCMToken(value: String) {
setStringPreference(TextSecurePreferences.FCM_TOKEN, value)
override fun setPushToken(value: String) {
setStringPreference(TextSecurePreferences.PUSH_TOKEN, value)
}
override fun getLastFCMUploadTime(): Long {
return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0)
override fun getPushRegisterTime(): Long {
return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0)
}
override fun setLastFCMUploadTime(value: Long) {
setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value)
override fun setPushRegisterTime(value: Long) {
setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value)
}
override fun isScreenLockEnabled(): Boolean {
@@ -1279,6 +1300,15 @@ class AppTextSecurePreferences @Inject constructor(
return getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null)
}
override fun getHasLegacyConfig(): Boolean {
return getBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, false)
}
override fun setHasLegacyConfig(newValue: Boolean) {
setBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, newValue)
TextSecurePreferences._events.tryEmit(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG)
}
override fun setLocalNumber(localNumber: String) {
setStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, localNumber.toLowerCase())
}
@@ -1422,6 +1452,9 @@ class AppTextSecurePreferences @Inject constructor(
setIntegerPreference(TextSecurePreferences.NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
}
override fun hasForcedNewConfig(): Boolean =
getBooleanPreference(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, false)
override fun getBooleanPreference(key: String?, defaultValue: Boolean): Boolean {
return getDefaultSharedPreferences(context).getBoolean(key, defaultValue)
}

View File

@@ -13,4 +13,4 @@ fun Context.getColorFromAttr(
): Int {
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
return typedValue.data
}
}

View File

@@ -1,8 +1,5 @@
package org.session.libsession.utilities.bencode
import org.session.libsession.utilities.bencode.Bencode.Decoder.Companion.DICT_INDICATOR
import org.session.libsession.utilities.bencode.Bencode.Decoder.Companion.END_INDICATOR
import org.session.libsession.utilities.bencode.Bencode.Decoder.Companion.LIST_INDICATOR
import java.util.LinkedList
object Bencode {
@@ -16,7 +13,7 @@ object Bencode {
* Decode an element based on next marker assumed to be string/int/list/dict or return null
*/
fun decode(): BencodeElement? {
val result = when (iterator.peek()) {
val result = when (iterator.peek()?.toInt()?.toChar()) {
in NUMBERS -> decodeString()
INT_INDICATOR -> decodeInt()
LIST_INDICATOR -> decodeList()
@@ -32,15 +29,15 @@ object Bencode {
* Decode a string element from iterator assumed to have structure `{length}:{data}`
*/
private fun decodeString(): BencodeString? {
val lengthStrings = buildList<Byte> {
while (iterator.isNotEmpty() && iterator.peek() != SEPARATOR) {
add(iterator.pop())
val lengthStrings = buildString {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
append(iterator.pop().toInt().toChar())
}
}.toByteArray()
}
iterator.pop() // drop `:`
val length = lengthStrings.decodeToString().toIntOrNull(10) ?: return null
val length = lengthStrings.toIntOrNull(10) ?: return null
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
return BencodeString(String(remaining))
return BencodeString(remaining)
}
/**
@@ -48,12 +45,12 @@ object Bencode {
*/
private fun decodeInt(): BencodeElement? {
iterator.pop() // drop `i`
val intString = buildList<Byte> {
while (iterator.isNotEmpty() && iterator.peek() != END_INDICATOR) {
add(iterator.pop())
val intString = buildString {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
append(iterator.pop().toInt().toChar())
}
}.toByteArray()
val asInt = intString.decodeToString().toIntOrNull(10) ?: return null
}
val asInt = intString.toIntOrNull(10) ?: return null
iterator.pop() // drop `e`
return BencodeInteger(asInt)
}
@@ -64,7 +61,7 @@ object Bencode {
private fun decodeList(): BencodeElement {
iterator.pop() // drop `l`
val listElements = mutableListOf<BencodeElement>()
while (iterator.isNotEmpty() && iterator.peek() != END_INDICATOR) {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
decode()?.let { nextElement ->
listElements += nextElement
}
@@ -79,22 +76,22 @@ object Bencode {
private fun decodeDict(): BencodeElement? {
iterator.pop() // drop `d`
val dictElements = mutableMapOf<String,BencodeElement>()
while (iterator.isNotEmpty() && iterator.peek() != END_INDICATOR) {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
val key = decodeString() ?: return null
val value = decode() ?: return null
dictElements += key.value to value
dictElements += key.value.decodeToString() to value
}
iterator.pop() // drop `e`
return BencodeDict(dictElements)
}
companion object {
val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9').map { it.code.toByte() }
const val INT_INDICATOR = 'i'.code.toByte()
const val LIST_INDICATOR = 'l'.code.toByte()
const val DICT_INDICATOR = 'd'.code.toByte()
const val END_INDICATOR = 'e'.code.toByte()
const val SEPARATOR = ':'.code.toByte()
private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
private const val INT_INDICATOR = 'i'
private const val LIST_INDICATOR = 'l'
private const val DICT_INDICATOR = 'd'
private const val END_INDICATOR = 'e'
private const val SEPARATOR = ':'
}
}
@@ -105,47 +102,68 @@ sealed class BencodeElement {
abstract fun encode(): ByteArray
}
fun String.bencode() = BencodeString(this)
fun String.bencode() = BencodeString(this.encodeToByteArray())
fun Int.bencode() = BencodeInteger(this)
data class BencodeString(val value: String): BencodeElement() {
data class BencodeString(val value: ByteArray): BencodeElement() {
override fun encode(): ByteArray = buildString {
append(value.length.toString())
append(value.size.toString())
append(':')
append(value)
}.toByteArray()
}.toByteArray() + value
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BencodeString
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
data class BencodeInteger(val value: Int): BencodeElement() {
override fun encode(): ByteArray = buildString {
append('i')
append(value)
append(value.toString())
append('e')
}.toByteArray()
}
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
constructor(vararg values: BencodeElement) : this(values.toList())
override fun encode(): ByteArray = buildList {
add(LIST_INDICATOR)
for (value in values) {
addAll(value.encode().toTypedArray())
}
add(END_INDICATOR)
}.toByteArray()
override fun encode(): ByteArray = "l".toByteArray() +
values.fold(byteArrayOf()) { array, element -> array + element.encode() } +
"e".toByteArray()
}
data class BencodeDict(val values: Map<String, BencodeElement>): BencodeElement() {
constructor(vararg values: Pair<String, BencodeElement>) : this(values.toMap())
override fun encode(): ByteArray = buildList {
add(DICT_INDICATOR)
for ((key, value) in values) {
addAll(BencodeString(key).encode().toTypedArray())
addAll(value.encode().toTypedArray())
}
add(END_INDICATOR)
}.toByteArray()
override fun encode(): ByteArray = "d".toByteArray() +
values.entries.fold(byteArrayOf()) { array, (key, value) ->
array + key.bencode().encode() + value.encode()
} + "e".toByteArray()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BencodeDict
if (values != other.values) return false
return true
}
override fun hashCode(): Int {
return values.hashCode()
}
}

View File

@@ -100,6 +100,7 @@ public class Recipient implements RecipientModifiedListener {
private boolean profileSharing;
private String notificationChannel;
private boolean forceSmsSelection;
private String wrapperHash;
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
@@ -284,6 +285,7 @@ public class Recipient implements RecipientModifiedListener {
this.profileSharing = details.profileSharing;
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
this.forceSmsSelection = details.forceSmsSelection;
this.wrapperHash = details.wrapperHash;
this.participants.addAll(details.participants);
this.resolving = false;
@@ -330,7 +332,7 @@ public class Recipient implements RecipientModifiedListener {
return contact.displayName(Contact.ContactContext.REGULAR);
} else {
Contact contact = storage.getContactWithSessionID(sessionID);
if (contact == null) { return sessionID; }
if (contact == null) { return null; }
return contact.displayName(Contact.ContactContext.REGULAR);
}
}
@@ -445,6 +447,10 @@ public class Recipient implements RecipientModifiedListener {
return address.isOpenGroup();
}
public boolean isOpenGroupOutboxRecipient() {
return address.isOpenGroupOutbox();
}
public boolean isOpenGroupInboxRecipient() {
return address.isOpenGroupInbox();
}
@@ -488,7 +494,13 @@ public class Recipient implements RecipientModifiedListener {
public synchronized String toShortString() {
String name = getName();
return (name != null ? name : address.serialize());
if (name != null) return name;
String sessionId = address.serialize();
if (sessionId.length() < 4) return sessionId; // so substrings don't throw out of bounds exceptions
int takeAmount = 4;
String start = sessionId.substring(0, takeAmount);
String end = sessionId.substring(sessionId.length()-takeAmount);
return start+"..."+end;
}
public synchronized @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) {
@@ -734,6 +746,14 @@ public class Recipient implements RecipientModifiedListener {
return unidentifiedAccessMode;
}
public String getWrapperHash() {
return wrapperHash;
}
public void setWrapperHash(String wrapperHash) {
this.wrapperHash = wrapperHash;
}
public void setUnidentifiedAccessMode(@NonNull UnidentifiedAccessMode unidentifiedAccessMode) {
synchronized (this) {
this.unidentifiedAccessMode = unidentifiedAccessMode;
@@ -756,12 +776,12 @@ public class Recipient implements RecipientModifiedListener {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Recipient recipient = (Recipient) o;
return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar);
return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar) && Objects.equals(wrapperHash, recipient.wrapperHash);
}
@Override
public int hashCode() {
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar);
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash);
result = 31 * result + Arrays.hashCode(profileKey);
return result;
}
@@ -866,6 +886,7 @@ public class Recipient implements RecipientModifiedListener {
private final String notificationChannel;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
private final String wrapperHash;
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
int notifyType,
@@ -888,7 +909,8 @@ public class Recipient implements RecipientModifiedListener {
boolean profileSharing,
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection)
boolean forceSmsSelection,
String wrapperHash)
{
this.blocked = blocked;
this.approved = approved;
@@ -915,6 +937,7 @@ public class Recipient implements RecipientModifiedListener {
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
this.wrapperHash = wrapperHash;
}
public @Nullable MaterialColor getColor() {
@@ -1016,6 +1039,11 @@ public class Recipient implements RecipientModifiedListener {
public boolean isForceSmsSelection() {
return forceSmsSelection;
}
public String getWrapperHash() {
return wrapperHash;
}
}

View File

@@ -178,6 +178,7 @@ class RecipientProvider {
@Nullable final String notificationChannel;
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
final boolean forceSmsSelection;
final String wrapperHash;
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
@@ -211,6 +212,7 @@ class RecipientProvider {
this.notificationChannel = settings != null ? settings.getNotificationChannel() : null;
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
if (name == null && settings != null) this.name = settings.getSystemDisplayName();
else this.name = name;

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- MessageRecord -->
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
<string name="MessageRecord_you">Vous</string>
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
<!-- expiration -->
<string name="expiration_off">Désactivé</string>
<plurals name="expiration_seconds">
<item quantity="one">%d seconde</item>
<item quantity="other">%d secondes</item>
</plurals>
<string name="expiration_seconds_abbreviated">%d s</string>
<plurals name="expiration_minutes">
<item quantity="one">%d minute</item>
<item quantity="other">%d minutes</item>
</plurals>
<string name="expiration_minutes_abbreviated">%d min</string>
<plurals name="expiration_hours">
<item quantity="one">%d heure</item>
<item quantity="other">%d heures</item>
</plurals>
<string name="expiration_hours_abbreviated">%d h</string>
<plurals name="expiration_days">
<item quantity="one">%d jour</item>
<item quantity="other">%d jours</item>
</plurals>
<string name="expiration_days_abbreviated">%d j</string>
<plurals name="expiration_weeks">
<item quantity="one">%d semaine</item>
<item quantity="other">%d semaines</item>
</plurals>
<string name="expiration_weeks_abbreviated">%d sem</string>
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
<!-- RecipientProvider -->
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
</resources>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- MessageRecord -->
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
<string name="MessageRecord_you">Vous</string>
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
<!-- expiration -->
<string name="expiration_off">Désactivé</string>
<plurals name="expiration_seconds">
<item quantity="one">%d seconde</item>
<item quantity="other">%d secondes</item>
</plurals>
<string name="expiration_seconds_abbreviated">%d s</string>
<plurals name="expiration_minutes">
<item quantity="one">%d minute</item>
<item quantity="other">%d minutes</item>
</plurals>
<string name="expiration_minutes_abbreviated">%d min</string>
<plurals name="expiration_hours">
<item quantity="one">%d heure</item>
<item quantity="other">%d heures</item>
</plurals>
<string name="expiration_hours_abbreviated">%d h</string>
<plurals name="expiration_days">
<item quantity="one">%d jour</item>
<item quantity="other">%d jours</item>
</plurals>
<string name="expiration_days_abbreviated">%d j</string>
<plurals name="expiration_weeks">
<item quantity="one">%d semaine</item>
<item quantity="other">%d semaines</item>
</plurals>
<string name="expiration_weeks_abbreviated">%d sem</string>
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
<!-- RecipientProvider -->
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
</resources>

View File

@@ -220,18 +220,6 @@
<attr name="emoji_maxLength" format="integer" />
</declare-styleable>
<declare-styleable name="ColorPickerPreference">
<attr name="currentColor" format="reference" />
<attr name="colors" format="reference" />
<attr name="sortColors" format="boolean|reference" />
<attr name="colorDescriptions" format="reference" />
<attr name="columns" format="integer|reference" />
<attr name="colorSize" format="enum|reference">
<enum name="large" value="1" />
<enum name="small" value="2" />
</attr>
</declare-styleable>
<declare-styleable name="VerificationCodeView">
<attr name="vcv_spacing" format="dimension"/>
<attr name="vcv_inputWidth" format="dimension"/>

View File

@@ -2,4 +2,5 @@
<resources>
<bool name="enable_alarm_manager">true</bool>
<bool name="enable_job_service">false</bool>
<bool name="screen_security_default">true</bool>
</resources>

View File

@@ -1,9 +1,9 @@
package org.session.libsession.utilities
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
class OpenGroupUrlParserTest {
class CommunityUrlParserTest {
@Test
fun parseUrlTest() {