SS-2168 - User profile warnings (#1531)

* Profile picture upload fail informs user

* End of day push

* Push before trying with okhttp library update

* WIP

* Further WIP

* Add additional debug comments

* Push before cleanup

* Cleaned up

* More cleanup

* Minor adjustment

* Final cleanup prior to PR review

* Removed commented out old conscrypt version import

* Addressed PR feeback from Fanchao

---------

Co-authored-by: alansley <aclansley@gmail.com>
This commit is contained in:
AL-Session 2024-07-09 16:23:26 +10:00 committed by GitHub
parent de7df58503
commit 2e3acd902f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 173 additions and 113 deletions

View File

@ -271,7 +271,7 @@ dependencies {
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'org.webrtc:google-webrtc:1.0.32006'
implementation "me.leolin:ShortcutBadger:1.1.16"

View File

@ -22,6 +22,7 @@ import androidx.core.view.isVisible
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
@ -289,7 +290,7 @@ class VisibleMessageContentView : ConstraintLayout {
// replace URLSpans with ModalURLSpans
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
val updatedUrl = urlSpan.url.let { HttpUrl.parse(it).toString() }
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
val activity = context as AppCompatActivity
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")

View File

@ -55,7 +55,7 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String SESSION_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing_approval";
private static final String CALL_RINGTONE = "call_ringtone";
private static final String CALL_VIBRATE = "call_vibrate";
@ -69,7 +69,7 @@ public class RecipientDatabase extends Database {
private static final String[] RECIPIENT_PROJECTION = new String[] {
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
SIGNAL_PROFILE_NAME, SESSION_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS
};
@ -97,7 +97,7 @@ public class RecipientDatabase extends Database {
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
SESSION_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
CALL_RINGTONE + " TEXT DEFAULT NULL, " +
CALL_VIBRATE + " INTEGER DEFAULT " + Recipient.VibrateState.DEFAULT.getId() + ", " +
@ -204,7 +204,7 @@ public class RecipientDatabase extends Database {
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SESSION_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
@ -361,7 +361,7 @@ public class RecipientDatabase extends Database {
public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar);
contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar);
updateOrInsert(recipient.getAddress(), contentValues);
recipient.resolve().setProfileAvatar(profileAvatar);
notifyRecipientListeners();

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
@ -10,6 +11,7 @@ import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
@ -91,8 +93,6 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
private const val TAG = "Storage"
@ -471,7 +471,8 @@ open class Storage(
val userPublicKey = getUserPublicKey() ?: return
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// update name
// Update profile name
val name = userProfile.getName() ?: return
val userPic = userProfile.getPic()
val profileManager = SSKEnvironment.shared.profileManager
@ -480,13 +481,14 @@ open class Storage(
profileManager.setName(context, recipient, name)
}
// update pfp
// Update profile picture
if (userPic == UserPic.DEFAULT) {
clearUserPic()
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
setUserProfilePicture(userPic.url, userPic.key)
}
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
// delete nts thread if needed
val ourThread = getThreadId(recipient) ?: return
@ -514,12 +516,13 @@ open class Storage(
addLibSessionContacts(extracted, messageTimestamp)
}
override fun clearUserPic() {
val userPublicKey = getUserPublicKey() ?: return
override fun clearUserPic() {
val userPublicKey = getUserPublicKey() ?: return Log.w(TAG, "No user public key when trying to clear user pic")
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// clear picture if userPic is null
// Clear details related to the user's profile picture
TextSecurePreferences.setProfileKey(context, null)
ProfileKeyUtil.setEncodedProfileKey(context, null)
recipientDatabase.setProfileAvatar(recipient, null)
@ -528,7 +531,6 @@ open class Storage(
Recipient.removeCached(fromSerialized(userPublicKey))
configFactory.user?.setPic(UserPic.DEFAULT)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) {

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.annotation.WorkerThread
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup
@ -143,9 +144,9 @@ object OpenGroupManager {
@WorkerThread
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
val url = HttpUrl.parse(urlAsString) ?: return null
val url = urlAsString.toHttpUrlOrNull() ?: return null
val server = OpenGroup.getServer(urlAsString)
val room = url.pathSegments().firstOrNull() ?: return null
val room = url.pathSegments.firstOrNull() ?: return null
val publicKey = url.queryParameter("public_key") ?: return null
return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function

View File

@ -35,6 +35,5 @@ public class AndroidLogger extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() {
}
public void blockUntilAllWritesFinished() { }
}

View File

@ -10,6 +10,7 @@ import kotlinx.serialization.json.decodeFromStream
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.Response
@ -99,7 +100,7 @@ class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver)
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
val server = Server.LATEST
val url = "${server.url}/$path"
val body = RequestBody.create(MediaType.get("application/json"), requestParameters)
val body = RequestBody.create("application/json".toMediaType(), requestParameters)
val request = Request.Builder().url(url).post(body).build()
return OnionRequestAPI.sendOnionRequest(

View File

@ -22,13 +22,16 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.security.SecureRandom
import javax.inject.Inject
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.avatars.ProfileContactPhoto
@ -37,7 +40,7 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.*
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.getProperty
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView
@ -56,12 +59,10 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import java.io.File
import java.security.SecureRandom
import javax.inject.Inject
@AndroidEntryPoint
class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val TAG = "SettingsActivity"
@Inject
lateinit var configFactory: ConfigFactory
@ -233,41 +234,78 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
displayName: String? = null
) {
binding.loader.isVisible = true
val promises = mutableListOf<Promise<*, Exception>>()
if (displayName != null) {
TextSecurePreferences.setProfileName(this, displayName)
configFactory.user?.setName(displayName)
}
// Bail if we're not updating the profile picture in any way
if (!isUpdatingProfilePicture) return
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
if (isUpdatingProfilePicture) {
if (profilePicture != null) {
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
} else {
val uploadProfilePicturePromise: Promise<*, Exception>
var removingProfilePic = false
// Adding a new profile picture?
if (profilePicture != null) {
uploadProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)
} else {
// If not then we must be removing the existing one.
// Note: To get a promise that will resolve / sync correctly we overwrite the existing profile picture with
// a 0 byte image.
removingProfilePic = true
val emptyByteArray = ByteArray(0)
uploadProfilePicturePromise = ProfilePictureUtilities.upload(emptyByteArray, encodedProfileKey, this)
}
// If the upload picture promise succeeded then we hit this successUi block
uploadProfilePicturePromise.successUi {
// If we successfully removed the profile picture on the network then we can clear the
// local data - otherwise it's weird to fail the online section but it _looks_ like it
// worked because we cleared the local image (also it denies them the chance to retry
// removal if we do it locally, and may result in them having a visible profile picture
// everywhere EXCEPT on their own device!).
if (removingProfilePic) {
MessagingModuleConfiguration.shared.storage.clearUserPic()
}
}
val compoundPromise = all(promises)
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
val userConfig = configFactory.user
if (isUpdatingProfilePicture) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config
val url = TextSecurePreferences.getProfilePictureURL(this)
val profileKey = ProfileKeyUtil.getProfileKey(this)
if (profilePicture == null) {
userConfig?.setPic(UserPic.DEFAULT)
} else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey))
}
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config
val url = TextSecurePreferences.getProfilePictureURL(this)
val profileKey = ProfileKeyUtil.getProfileKey(this)
// If we have a URL and a profile key then set the user's profile picture
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey))
}
if (userConfig != null && userConfig.needsDump()) {
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
compoundPromise.alwaysUi {
// Or if the promise failed to upload the new profile picture then we hit this failUi block
uploadProfilePicturePromise.failUi {
if (removingProfilePic) {
Log.e(TAG, "Failed to remove profile picture")
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
} else {
Log.e(TAG, "Failed to upload profile picture")
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
}
}
// Finally, regardless of whether the promise succeeded or failed, we always hit this `alwaysUi` block
uploadProfilePicturePromise.alwaysUi {
if (displayName != null) {
binding.btnGroupNameDisplay.text = displayName
}

View File

@ -24,6 +24,7 @@ import org.session.libsession.utilities.WindowDebouncer
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
@ -31,10 +32,12 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.Timer
object ConfigurationMessageUtilities {
private const val TAG = "ConfigMessageUtils"
private val debouncer = WindowDebouncer(3000, Timer())
private fun scheduleConfigSync(userPublicKey: String) {
debouncer.publish {
// don't schedule job if we already have one
val storage = MessagingModuleConfiguration.shared.storage
@ -44,23 +47,20 @@ object ConfigurationMessageUtilities {
(currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
return@publish
}
val newConfigSync = ConfigurationSyncJob(ourDestination)
JobQueue.shared.add(newConfigSync)
val newConfigSyncJob = ConfigurationSyncJob(ourDestination)
JobQueue.shared.add(newConfigSyncJob)
}
}
@JvmStatic
fun syncConfigurationIfNeeded(context: Context) {
// add if check here to schedule new config job process and return early
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Log.w(TAG, "User Public Key is null")
scheduleConfigSync(userPublicKey)
}
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
// add if check here to schedule new config job process and return early
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofFail(NullPointerException("User Public Key is null"))
// schedule job if none exist
// don't schedule job if we already have one
// Schedule a new job if one doesn't already exist (only)
scheduleConfigSync(userPublicKey)
return Promise.ofSuccess(Unit)
}

View File

@ -21,7 +21,7 @@ googleServicesVersion=4.3.12
kotlinVersion=1.8.21
android.useAndroidX=true
appcompatVersion=1.6.1
coreVersion=1.8.0
coreVersion=1.13.1
coroutinesVersion=1.6.4
curve25519Version=0.6.0
daggerVersion=2.46.1
@ -33,7 +33,7 @@ kovenantVersion=3.3.0
lifecycleVersion=2.5.1
materialVersion=1.8.0
mockitoKotlinVersion=4.1.0
okhttpVersion=3.12.1
okhttpVersion=4.12.0
pagingVersion=3.0.0
preferenceVersion=1.2.0
protobufVersion=2.5.0

View File

@ -3,8 +3,11 @@ package org.session.libsession.messaging.file_server
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.HTTP
@ -37,18 +40,18 @@ object FileServerApi {
)
private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? {
if (body != null) return RequestBody.create(MediaType.get("application/octet-stream"), body)
if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body)
if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
return RequestBody.create("application/json".toMediaType(), parametersAsJSON)
}
private fun send(request: Request): Promise<ByteArray, Exception> {
val url = HttpUrl.parse(server) ?: return Promise.ofFail(Error.InvalidURL)
val url = server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL)
val urlBuilder = HttpUrl.Builder()
.scheme(url.scheme())
.host(url.host())
.port(url.port())
.scheme(url.scheme)
.host(url.host)
.port(url.port)
.addPathSegments(request.endpoint)
if (request.verb == HTTP.Verb.GET) {
for ((key, value) in request.queryParameters) {
@ -57,7 +60,7 @@ object FileServerApi {
}
val requestBuilder = okhttp3.Request.Builder()
.url(urlBuilder.build())
.headers(Headers.of(request.headers))
.headers(request.headers.toHeaders())
when (request.verb) {
HTTP.Verb.GET -> requestBuilder.get()
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!)

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.MessagingModuleConfiguration
@ -141,8 +142,8 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
DownloadUtilities.downloadFile(tempFile, attachment.url)
} else {
Log.d("AttachmentDownloadJob", "downloading open group attachment")
val url = HttpUrl.parse(attachment.url)!!
val fileID = url.pathSegments().last()
val url = attachment.url.toHttpUrlOrNull()!!
val fileID = url.pathSegments.last()
OpenGroupApi.download(fileID, openGroup.room, openGroup.server).get().let {
tempFile.writeBytes(it)
}

View File

@ -176,7 +176,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE)
val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE_BYTES)
kryo.writeClassAndObject(output, message)
output.close()
return Data.Builder()

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.utilities.Data
@ -21,9 +22,9 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
override val maxFailureCount: Int = 1
val openGroupId: String? get() {
val url = HttpUrl.parse(joinUrl) ?: return null
val url = joinUrl.toHttpUrlOrNull() ?: return null
val server = OpenGroup.getServer(joinUrl)?.toString()?.removeSuffix("/") ?: return null
val room = url.pathSegments().firstOrNull() ?: return null
val room = url.pathSegments.firstOrNull() ?: return null
return "$server.$room"
}

View File

@ -1,5 +1,6 @@
package org.session.libsession.messaging.jobs
import java.util.concurrent.atomic.AtomicBoolean
import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor
import nl.komponents.kovenant.functional.bind
import org.session.libsession.messaging.MessagingModuleConfiguration
@ -10,7 +11,6 @@ import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.RawResponse
import org.session.libsession.snode.SnodeAPI
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 {
@ -180,7 +180,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
// type mappings
const val CONTACT_TYPE = 1
const val GROUP_TYPE = 2
}
class Factory: Job.Factory<ConfigurationSyncJob> {

View File

@ -14,7 +14,7 @@ interface Job {
// Keys used for database storage
private val ID_KEY = "id"
private val FAILURE_COUNT_KEY = "failure_count"
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
internal const val MAX_BUFFER_SIZE_BYTES = 1_000_000 // ~1MB
}
suspend fun execute(dispatcherName: String)

View File

@ -4,7 +4,7 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE_BYTES
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.visible.VisibleMessage
@ -118,12 +118,12 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val kryo = Kryo()
kryo.isRegistrationRequired = false
// Message
val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE)
val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE_BYTES)
kryo.writeClassAndObject(messageOutput, message)
messageOutput.close()
val serializedMessage = messageOutput.toBytes()
// Destination
val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE)
val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE_BYTES)
kryo.writeClassAndObject(destinationOutput, destination)
destinationOutput.close()
val serializedDestination = destinationOutput.toBytes()

View File

@ -3,10 +3,10 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE_BYTES
import org.session.libsession.messaging.sending_receiving.notifications.Server
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.OnionRequestAPI
@ -33,7 +33,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val server = Server.LEGACY
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
val url = "${server.url}/notify"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()
retryIfNeeded(4) {
OnionRequestAPI.sendOnionRequest(
@ -67,7 +67,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val output = Output(serializedMessage, MAX_BUFFER_SIZE)
val output = Output(serializedMessage, MAX_BUFFER_SIZE_BYTES)
kryo.writeObject(output, message)
output.close()
return Data.Builder()

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.open_groups
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import java.util.Locale
@ -47,11 +48,11 @@ data class OpenGroup(
}
fun getServer(urlAsString: String): HttpUrl? {
val url = HttpUrl.parse(urlAsString) ?: return null
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
if (url.port() != 80 || url.port() != 443) {
val url = urlAsString.toHttpUrlOrNull() ?: return null
val builder = HttpUrl.Builder().scheme(url.scheme).host(url.host)
if (url.port != 80 || url.port != 443) {
// Non-standard port; add to server
builder.port(url.port())
builder.port(url.port)
}
return builder.build()
}

View File

@ -14,8 +14,11 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
@ -282,10 +285,10 @@ object OpenGroupApi {
)
private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? {
if (body != null) return RequestBody.create(MediaType.get("application/octet-stream"), body)
if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body)
if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
return RequestBody.create("application/json".toMediaType(), parametersAsJSON)
}
private fun getResponseBody(request: Request): Promise<ByteArray, Exception> {
@ -301,7 +304,7 @@ object OpenGroupApi {
}
private fun send(request: Request): Promise<OnionResponse, Exception> {
HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
request.server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL)
val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}")
if (request.verb == GET && request.queryParameters.isNotEmpty()) {
urlBuilder.append("?")
@ -387,7 +390,7 @@ object OpenGroupApi {
val requestBuilder = okhttp3.Request.Builder()
.url(urlRequest)
.headers(Headers.of(headers))
.headers(headers.toHeaders())
when (request.verb) {
GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!)

View File

@ -3,6 +3,7 @@ package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint
import nl.komponents.kovenant.Promise
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
@ -58,7 +59,7 @@ object PushRegistryV1 {
val url = "${server.url}/register_legacy_groups_only"
val body = RequestBody.create(
MediaType.get("application/json"),
"application/json".toMediaType(),
JsonUtil.toJson(parameters)
)
val request = Request.Builder().url(url).post(body).build()
@ -83,7 +84,7 @@ object PushRegistryV1 {
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 body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()
sendOnionRequest(request) success {
@ -120,7 +121,7 @@ object PushRegistryV1 {
): 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 body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()
return retryIfNeeded(maxRetryCount) {

View File

@ -467,9 +467,9 @@ object OnionRequestAPI {
x25519PublicKey: String,
version: Version = Version.V4
): Promise<OnionResponse, Exception> {
val url = request.url()
val url = request.url
val payload = generatePayload(request, server, version)
val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port())
val destination = Destination.Server(url.host, version.value, x25519PublicKey, url.scheme, url.port)
return sendOnionRequest(destination, payload, version).recover { exception ->
Log.d("Loki", "Couldn't reach server: $url due to error: $exception.")
throw exception
@ -478,7 +478,7 @@ object OnionRequestAPI {
private fun generatePayload(request: Request, server: String, version: Version): ByteArray {
val headers = request.getHeadersForOnionRequest().toMutableMap()
val url = request.url()
val url = request.url
val urlAsString = url.toString()
val body = request.getBodyForOnionRequest() ?: "null"
val endpoint = when {
@ -486,19 +486,19 @@ object OnionRequestAPI {
else -> ""
}
return if (version == Version.V4) {
if (request.body() != null &&
if (request.body != null &&
headers.keys.find { it.equals("Content-Type", true) } == null) {
headers["Content-Type"] = "application/json"
}
val requestPayload = mapOf(
"endpoint" to endpoint,
"method" to request.method(),
"method" to request.method,
"headers" to headers
)
val requestData = JsonUtil.toJson(requestPayload).toByteArray()
val prefixData = "l${requestData.size}:".toByteArray(Charsets.US_ASCII)
val suffixData = "e".toByteArray(Charsets.US_ASCII)
if (request.body() != null) {
if (request.body != null) {
val bodyData = if (body is ByteArray) body else body.toString().toByteArray()
val bodyLengthData = "${bodyData.size}:".toByteArray(Charsets.US_ASCII)
prefixData + requestData + bodyLengthData + bodyData + suffixData
@ -509,7 +509,7 @@ object OnionRequestAPI {
val payload = mapOf(
"body" to body,
"endpoint" to endpoint.removePrefix("/"),
"method" to request.method(),
"method" to request.method,
"headers" to headers
)
JsonUtil.toJson(payload).toByteArray()

View File

@ -9,13 +9,13 @@ import java.util.Locale
internal fun Request.getHeadersForOnionRequest(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
val contentType = body()?.contentType()
val contentType = body?.contentType()
if (contentType != null) {
result["content-type"] = contentType.toString()
}
val headers = headers()
val headers = headers
for (name in headers.names()) {
val value = headers.get(name)
val value = headers[name]
if (value != null) {
if (value.toLowerCase(Locale.US) == "true" || value.toLowerCase(Locale.US) == "false") {
result[name] = value.toBoolean()
@ -33,7 +33,7 @@ internal fun Request.getBodyForOnionRequest(): Any? {
try {
val copyOfThis = newBuilder().build()
val buffer = Buffer()
val body = copyOfThis.body() ?: return null
val body = copyOfThis.body ?: return null
body.writeTo(buffer)
val bodyAsData = buffer.readByteArray()
if (body is MultipartBody) {

View File

@ -1,6 +1,7 @@
package org.session.libsession.utilities
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
@ -36,8 +37,8 @@ object DownloadUtilities {
*/
@JvmStatic
fun downloadFile(outputStream: OutputStream, urlAsString: String) {
val url = HttpUrl.parse(urlAsString)!!
val fileID = url.pathSegments().last()
val url = urlAsString.toHttpUrlOrNull()!!
val fileID = url.pathSegments.last()
try {
FileServerApi.download(fileID).get().let {
outputStream.write(it)

View File

@ -1,6 +1,7 @@
package org.session.libsession.utilities
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.open_groups.migrateLegacyServerUrl
object OpenGroupUrlParser {
@ -19,14 +20,14 @@ object OpenGroupUrlParser {
// URL has to start with 'http://'
val urlWithPrefix = if (!string.startsWith("http")) "http://$string" else string
// If the URL is malformed, throw an exception
val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL
val url = urlWithPrefix.toHttpUrlOrNull() ?: throw Error.MalformedURL
// Parse components
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix).migrateLegacyServerUrl()
val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
val server = HttpUrl.Builder().scheme(url.scheme).host(url.host).port(url.port).build().toString().removeSuffix(suffix).migrateLegacyServerUrl()
val room = url.pathSegments.firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey
if (publicKey.length != 64) throw Error.InvalidPublicKey
// Return
return V2OpenGroupInfo(server,room,publicKey)
return V2OpenGroupInfo(server, room, publicKey)
}
fun trimQueryParameter(string: String): String {

View File

@ -9,7 +9,7 @@ import java.util.concurrent.atomic.AtomicReference
* Not really a 'debouncer' but named to be similar to the current Debouncer
* designed to queue tasks on a window (if not already queued) like a timer
*/
class WindowDebouncer(private val window: Long, private val timer: Timer) {
class WindowDebouncer(private val timeWindowMilliseconds: Long, private val timer: Timer) {
private val atomicRef: AtomicReference<Runnable?> = AtomicReference(null)
private val hasStarted = AtomicBoolean(false)
@ -23,7 +23,7 @@ class WindowDebouncer(private val window: Long, private val timer: Timer) {
fun publish(runnable: Runnable) {
if (hasStarted.compareAndSet(false, true)) {
timer.scheduleAtFixedRate(recursiveRunnable, 0, window)
timer.scheduleAtFixedRate(recursiveRunnable, 0, timeWindowMilliseconds)
}
atomicRef.compareAndSet(null, runnable)
}

View File

@ -80,4 +80,7 @@
<string name="clearDevice">Clear Device</string>
<string name="clearDeviceOnly">Clear device only</string>
<string name="clearDeviceAndNetwork">Clear device and network</string>
<string name="profileDisplayPictureRemoveError">Failed to remove display picture.</string>
<string name="profileErrorUpdate">Failed to update profile.</string>
</resources>

View File

@ -1,6 +1,7 @@
package org.session.libsignal.utilities
import okhttp3.MediaType
import android.util.Log
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
@ -11,10 +12,12 @@ import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
object HTTP {
var isConnectedToNetwork: (() -> Boolean) = { false }
private val seedNodeConnection by lazy {
OkHttpClient().newBuilder()
.callTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
@ -106,7 +109,7 @@ object HTTP {
Verb.GET -> request.get()
Verb.PUT, Verb.POST -> {
if (body == null) { throw Exception("Invalid request body.") }
val contentType = MediaType.get("application/json; charset=utf-8")
val contentType = "application/json; charset=utf-8".toMediaType()
@Suppress("NAME_SHADOWING") val body = RequestBody.create(contentType, body)
if (verb == Verb.PUT) request.put(body) else request.post(body)
}
@ -114,7 +117,7 @@ object HTTP {
}
lateinit var response: Response
try {
val connection = if (timeout != HTTP.timeout) { // Custom timeout
val connection: OkHttpClient = if (timeout != HTTP.timeout) { // Custom timeout
if (useSeedNodeConnection) {
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
}
@ -122,6 +125,7 @@ object HTTP {
} else {
if (useSeedNodeConnection) seedNodeConnection else defaultConnection
}
response = connection.newCall(request.build()).execute()
} catch (exception: Exception) {
Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.")
@ -131,9 +135,9 @@ object HTTP {
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}")
}
return when (val statusCode = response.code()) {
return when (val statusCode = response.code) {
200 -> {
response.body()?.bytes() ?: throw Exception("An error occurred.")
response.body!!.bytes()
}
else -> {
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")