Merge branch 'dev' into disappear-2

This commit is contained in:
andrew
2023-08-28 17:13:00 +09:30
105 changed files with 2323 additions and 2058 deletions

View File

@@ -43,6 +43,7 @@ interface StorageProtocol {
fun getUserProfile(): Profile
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
fun clearUserPic()
// Signal
@@ -232,4 +233,5 @@ interface StorageProtocol {
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
fun isCheckingCommunityRequests(): Boolean
}

View File

@@ -5,10 +5,12 @@ 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 configFactory: ConfigFactoryProtocol

View File

@@ -3,12 +3,11 @@ 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.OnionRequestAPI
import org.session.libsession.snode.SnodeMessage
@@ -31,23 +30,27 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
}
override suspend fun execute(dispatcherName: String) {
val server = PushNotificationAPI.server
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 {
} success {
handleSuccess(dispatcherName)
}. fail {
} fail {
handleFailure(dispatcherName, it)
}
}

View File

@@ -22,7 +22,8 @@ class VisibleMessage(
var profile: Profile? = null,
var openGroupInvitation: OpenGroupInvitation? = null,
var reaction: Reaction? = null,
var hasMention: Boolean = false
var hasMention: Boolean = false,
var blocksMessageRequests: Boolean = false
) : Message() {
override val isSelfSendValid: Boolean = true
@@ -71,6 +72,9 @@ class VisibleMessage(
val reaction = Reaction.fromProto(reactionProto)
result.reaction = reaction
}
result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests }
return result
}
}
@@ -130,6 +134,8 @@ class VisibleMessage(
return null
}
}
// Community blocked message requests flag
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
// Sync target
if (syncTarget != null) {
dataMessage.syncTarget = syncTarget

View File

@@ -753,7 +753,8 @@ object OpenGroupApi {
)
}
val serverCapabilities = storage.getServerCapabilities(server)
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
val isAcceptingCommunityRequests = storage.isCheckingCommunityRequests()
if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && isAcceptingCommunityRequests) {
requests.add(
if (lastInboxMessageId == null) {
BatchRequestInfo(

View File

@@ -30,6 +30,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
@@ -266,9 +267,16 @@ object MessageSender {
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
val storage = MessagingModuleConfiguration.shared.storage
val configFactory = MessagingModuleConfiguration.shared.configFactory
if (message.sentTimestamp == null) {
message.sentTimestamp = SnodeAPI.nowWithOffset
}
// Attach the blocks message requests info
configFactory.user?.let { user ->
if (message is VisibleMessage) {
message.blocksMessageRequests = !user.getCommunityMessageRequests()
}
}
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
var serverCapabilities = listOf<String>()
var blindedPublicKey: ByteArray? = null
@@ -479,8 +487,8 @@ object MessageSender {
}
// 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,11 +8,12 @@ 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.libsignal.crypto.ecc.Curve
@@ -32,7 +33,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
@@ -88,7 +93,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
// 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

View File

@@ -25,7 +25,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
@@ -397,6 +397,10 @@ fun MessageReceiver.handleVisibleMessage(
profileManager.setProfilePicture(context, recipient, null, null)
}
}
if (userPublicKey != messageSender && !isUserBlindedSender) {
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
}
}
// Parse quote if needed
var quoteModel: QuoteModel? = null
@@ -649,10 +653,14 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
// Set expiration timer
storage.setExpirationTimer(groupID, expireTimer)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Create thread
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.setThreadDate(threadId, formationTimestamp)
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
// Notify the user
if (userPublicKey == sender && !groupExists) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
} else if (userPublicKey != sender) {
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
}
// Start polling
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
}
@@ -958,7 +966,7 @@ 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)

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

@@ -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

@@ -686,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

@@ -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

@@ -1,5 +1,7 @@
package org.session.libsession.utilities
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex
import java.io.IOException
@@ -15,8 +17,15 @@ object GroupUtil {
}
@JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String {
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)
fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address {
val openGroupInboxId =
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
return getEncodedOpenGroupInboxID(openGroupInboxId)
}
@JvmStatic
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
}
@JvmStatic
@@ -51,7 +60,7 @@ object GroupUtil {
}
@JvmStatic
fun getDecodedOpenGroupInbox(groupID: String): String {
fun getDecodedOpenGroupInboxSessionId(groupID: String): String {
val decodedGroupId = getDecodedGroupID(groupID)
if (decodedGroupId.split("!").count() > 2) {
return decodedGroupId.split("!", limit = 3)[2]

View File

@@ -37,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
@@ -251,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"
@@ -287,6 +287,8 @@ interface TextSecurePreferences {
const val OCEAN_DARK = "ocean.dark"
const val OCEAN_LIGHT = "ocean.light"
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
@@ -309,31 +311,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
@@ -1008,7 +1010,6 @@ interface TextSecurePreferences {
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
}
}
}
@@ -1033,28 +1034,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 {

View File

@@ -0,0 +1,169 @@
package org.session.libsession.utilities.bencode
import java.util.LinkedList
object Bencode {
class Decoder(source: ByteArray) {
private val iterator = LinkedList<Byte>().apply {
addAll(source.asIterable())
}
/**
* 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()?.toInt()?.toChar()) {
in NUMBERS -> decodeString()
INT_INDICATOR -> decodeInt()
LIST_INDICATOR -> decodeList()
DICT_INDICATOR -> decodeDict()
else -> {
null
}
}
return result
}
/**
* Decode a string element from iterator assumed to have structure `{length}:{data}`
*/
private fun decodeString(): BencodeString? {
val lengthStrings = buildString {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
append(iterator.pop().toInt().toChar())
}
}
iterator.pop() // drop `:`
val length = lengthStrings.toIntOrNull(10) ?: return null
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
return BencodeString(remaining)
}
/**
* Decode an int element from iterator assumed to have structure `i{int}e`
*/
private fun decodeInt(): BencodeElement? {
iterator.pop() // drop `i`
val intString = buildString {
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
append(iterator.pop().toInt().toChar())
}
}
val asInt = intString.toIntOrNull(10) ?: return null
iterator.pop() // drop `e`
return BencodeInteger(asInt)
}
/**
* Decode a list element from iterator assumed to have structure `l{data}e`
*/
private fun decodeList(): BencodeElement {
iterator.pop() // drop `l`
val listElements = mutableListOf<BencodeElement>()
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
decode()?.let { nextElement ->
listElements += nextElement
}
}
iterator.pop() // drop `e`
return BencodeList(listElements)
}
/**
* Decode a dict element from iterator assumed to have structure `d{data}e`
*/
private fun decodeDict(): BencodeElement? {
iterator.pop() // drop `d`
val dictElements = mutableMapOf<String,BencodeElement>()
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
val key = decodeString() ?: return null
val value = decode() ?: return null
dictElements += key.value.decodeToString() to value
}
iterator.pop() // drop `e`
return BencodeDict(dictElements)
}
companion object {
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 = ':'
}
}
}
sealed class BencodeElement {
abstract fun encode(): ByteArray
}
fun String.bencode() = BencodeString(this.encodeToByteArray())
fun Int.bencode() = BencodeInteger(this)
data class BencodeString(val value: ByteArray): BencodeElement() {
override fun encode(): ByteArray = buildString {
append(value.size.toString())
append(':')
}.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.toString())
append('e')
}.toByteArray()
}
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
constructor(vararg values: BencodeElement) : this(values.toList())
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 = "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

@@ -101,6 +101,7 @@ public class Recipient implements RecipientModifiedListener {
private String notificationChannel;
private boolean forceSmsSelection;
private String wrapperHash;
private boolean blocksCommunityMessageRequests;
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
@@ -194,6 +195,7 @@ public class Recipient implements RecipientModifiedListener {
this.unidentifiedAccessMode = details.get().unidentifiedAccessMode;
this.forceSmsSelection = details.get().forceSmsSelection;
this.notifyType = details.get().notifyType;
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
this.disappearingState = details.get().disappearingState;
this.participants.clear();
@@ -232,6 +234,7 @@ public class Recipient implements RecipientModifiedListener {
Recipient.this.forceSmsSelection = result.forceSmsSelection;
Recipient.this.notifyType = result.notifyType;
Recipient.this.disappearingState = result.disappearingState;
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
Recipient.this.participants.clear();
Recipient.this.participants.addAll(result.participants);
@@ -285,6 +288,7 @@ public class Recipient implements RecipientModifiedListener {
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
this.forceSmsSelection = details.forceSmsSelection;
this.wrapperHash = details.wrapperHash;
this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests;
this.participants.addAll(details.participants);
this.resolving = false;
@@ -325,7 +329,7 @@ public class Recipient implements RecipientModifiedListener {
return this.name;
}
} else if (isOpenGroupInboxRecipient()){
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID);
String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID);
Contact contact = storage.getContactWithSessionID(inboxID);
if (contact == null) { return sessionID; }
return contact.displayName(Contact.ContactContext.REGULAR);
@@ -349,6 +353,18 @@ public class Recipient implements RecipientModifiedListener {
if (notify) notifyListeners();
}
public boolean getBlocksCommunityMessageRequests() {
return blocksCommunityMessageRequests;
}
public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) {
synchronized (this) {
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
}
notifyListeners();
}
public synchronized @NonNull MaterialColor getColor() {
if (isGroupRecipient()) return MaterialColor.GROUP;
else if (color != null) return color;
@@ -779,12 +795,43 @@ 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) && Objects.equals(wrapperHash, recipient.wrapperHash);
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)
&& blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests;
}
@Override
public int hashCode() {
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash);
int result = Objects.hash(
address,
name,
customLabel,
resolving,
groupAvatarId,
mutedUntil,
notifyType,
blocked,
approved,
approvedMe,
expireMessages,
profileName,
profileAvatar,
wrapperHash,
blocksCommunityMessageRequests
);
result = 31 * result + Arrays.hashCode(profileKey);
return result;
}
@@ -908,57 +955,61 @@ public class Recipient implements RecipientModifiedListener {
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
private final String wrapperHash;
private final boolean blocksCommunityMessageRequests;
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
int notifyType,
@NonNull DisappearingState disappearingState,
int notifyType,
@NonNull DisappearingState disappearingState,
@NonNull VibrateState messageVibrateState,
@NonNull VibrateState callVibrateState,
@Nullable Uri messageRingtone,
@Nullable Uri callRingtone,
@Nullable MaterialColor color,
int defaultSubscriptionId,
int expireMessages,
@NonNull RegisteredState registered,
@Nullable byte[] profileKey,
@Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@Nullable String systemContactUri,
@Nullable String signalProfileName,
@Nullable String signalProfileAvatar,
boolean profileSharing,
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection,
String wrapperHash)
@NonNull VibrateState callVibrateState,
@Nullable Uri messageRingtone,
@Nullable Uri callRingtone,
@Nullable MaterialColor color,
int defaultSubscriptionId,
int expireMessages,
@NonNull RegisteredState registered,
@Nullable byte[] profileKey,
@Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@Nullable String systemContactUri,
@Nullable String signalProfileName,
@Nullable String signalProfileAvatar,
boolean profileSharing,
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection,
String wrapperHash,
boolean blocksCommunityMessageRequests
)
{
this.blocked = blocked;
this.approved = approved;
this.approvedMe = approvedMe;
this.muteUntil = muteUntil;
this.notifyType = notifyType;
this.blocked = blocked;
this.approved = approved;
this.approvedMe = approvedMe;
this.muteUntil = muteUntil;
this.notifyType = notifyType;
this.disappearingState = disappearingState;
this.messageVibrateState = messageVibrateState;
this.callVibrateState = callVibrateState;
this.messageRingtone = messageRingtone;
this.callRingtone = callRingtone;
this.color = color;
this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
this.systemContactUri = systemContactUri;
this.signalProfileName = signalProfileName;
this.signalProfileAvatar = signalProfileAvatar;
this.profileSharing = profileSharing;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
this.wrapperHash = wrapperHash;
this.messageVibrateState = messageVibrateState;
this.callVibrateState = callVibrateState;
this.messageRingtone = messageRingtone;
this.callRingtone = callRingtone;
this.color = color;
this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
this.systemContactUri = systemContactUri;
this.signalProfileName = signalProfileName;
this.signalProfileAvatar = signalProfileAvatar;
this.profileSharing = profileSharing;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
this.wrapperHash = wrapperHash;
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
}
public @Nullable MaterialColor getColor() {
@@ -1065,6 +1116,10 @@ public class Recipient implements RecipientModifiedListener {
return wrapperHash;
}
public boolean getBlocksCommunityMessageRequests() {
return blocksCommunityMessageRequests;
}
}

View File

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

View File

@@ -0,0 +1,107 @@
package org.session.libsession.utilities
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import org.session.libsession.utilities.bencode.Bencode
import org.session.libsession.utilities.bencode.BencodeDict
import org.session.libsession.utilities.bencode.BencodeInteger
import org.session.libsession.utilities.bencode.BencodeList
import org.session.libsession.utilities.bencode.bencode
class BencoderTest {
@Test
fun `it should decode a basic string`() {
val basicString = "5:howdy".toByteArray()
val bencoder = Bencode.Decoder(basicString)
val result = bencoder.decode()
assertEquals("howdy".bencode(), result)
}
@Test
fun `it should decode a basic integer`() {
val basicInteger = "i3e".toByteArray()
val bencoder = Bencode.Decoder(basicInteger)
val result = bencoder.decode()
assertEquals(BencodeInteger(3), result)
}
@Test
fun `it should decode a list of integers`() {
val basicIntList = "li1ei2ee".toByteArray()
val bencoder = Bencode.Decoder(basicIntList)
val result = bencoder.decode()
assertEquals(
BencodeList(
1.bencode(),
2.bencode()
),
result
)
}
@Test
fun `it should decode a basic dict`() {
val basicDict = "d4:spaml1:a1:bee".toByteArray()
val bencoder = Bencode.Decoder(basicDict)
val result = bencoder.decode()
assertEquals(
BencodeDict(
"spam" to BencodeList(
"a".bencode(),
"b".bencode()
)
),
result
)
}
@Test
fun `it should encode a basic string`() {
val basicString = "5:howdy".toByteArray()
val element = "howdy".bencode()
assertArrayEquals(basicString, element.encode())
}
@Test
fun `it should encode a basic int`() {
val basicInt = "i3e".toByteArray()
val element = 3.bencode()
assertArrayEquals(basicInt, element.encode())
}
@Test
fun `it should encode a basic list`() {
val basicList = "li1ei2ee".toByteArray()
val element = BencodeList(1.bencode(),2.bencode())
assertArrayEquals(basicList, element.encode())
}
@Test
fun `it should encode a basic dict`() {
val basicDict = "d4:spaml1:a1:bee".toByteArray()
val element = BencodeDict(
"spam" to BencodeList(
"a".bencode(),
"b".bencode()
)
)
assertArrayEquals(basicDict, element.encode())
}
@Test
fun `it should encode a more complex real world case`() {
val source = "d15:lastReadMessaged66:031122334455667788990011223344556677889900112233445566778899001122i1234568790e66:051122334455667788990011223344556677889900112233445566778899001122i1234568790ee5:seqNoi1ee".toByteArray()
val result = Bencode.Decoder(source).decode()
val expected = BencodeDict(
"lastReadMessage" to BencodeDict(
"051122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode(),
"031122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode()
),
"seqNo" to BencodeInteger(1)
)
assertEquals(expected, result)
}
}