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>
pull/1542/head
AL-Session 9 months ago committed by GitHub
parent de7df58503
commit 2e3acd902f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

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

@ -55,7 +55,7 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_PHONE_LABEL = "system_phone_label"; private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
private static final String SYSTEM_CONTACT_URI = "system_contact_uri"; 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_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 PROFILE_SHARING = "profile_sharing_approval";
private static final String CALL_RINGTONE = "call_ringtone"; private static final String CALL_RINGTONE = "call_ringtone";
private static final String CALL_VIBRATE = "call_vibrate"; 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[] { 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, 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, 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, UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS 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, " + SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " + PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " 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, " + PROFILE_SHARING + " INTEGER DEFAULT 0, " +
CALL_RINGTONE + " TEXT DEFAULT NULL, " + CALL_RINGTONE + " TEXT DEFAULT NULL, " +
CALL_VIBRATE + " INTEGER DEFAULT " + Recipient.VibrateState.DEFAULT.getId() + ", " + 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 systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI)); String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); 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; boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL)); String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); 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) { public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar);
updateOrInsert(recipient.getAddress(), contentValues); updateOrInsert(recipient.getAddress(), contentValues);
recipient.resolve().setProfileAvatar(profileAvatar); recipient.resolve().setProfileAvatar(profileAvatar);
notifyRecipientListeners(); notifyRecipientListeners();

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

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

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

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

@ -22,13 +22,16 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint 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.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding import network.loki.messenger.databinding.ActivitySettingsBinding
import network.loki.messenger.libsession_util.util.UserPic import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.avatars.ProfileContactPhoto 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.*
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.recipients.Recipient 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.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView 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.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import java.io.File
import java.security.SecureRandom
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SettingsActivity : PassphraseRequiredActionBarActivity() { class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val TAG = "SettingsActivity"
@Inject @Inject
lateinit var configFactory: ConfigFactory lateinit var configFactory: ConfigFactory
@ -233,41 +234,78 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
displayName: String? = null displayName: String? = null
) { ) {
binding.loader.isVisible = true binding.loader.isVisible = true
val promises = mutableListOf<Promise<*, Exception>>()
if (displayName != null) { if (displayName != null) {
TextSecurePreferences.setProfileName(this, displayName) TextSecurePreferences.setProfileName(this, displayName)
configFactory.user?.setName(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) val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
if (isUpdatingProfilePicture) {
if (profilePicture != null) { val uploadProfilePicturePromise: Promise<*, Exception>
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) var removingProfilePic = false
} else {
// 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() 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 val userConfig = configFactory.user
if (isUpdatingProfilePicture) { AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config // new config
val url = TextSecurePreferences.getProfilePictureURL(this) val url = TextSecurePreferences.getProfilePictureURL(this)
val profileKey = ProfileKeyUtil.getProfileKey(this) val profileKey = ProfileKeyUtil.getProfileKey(this)
if (profilePicture == null) {
userConfig?.setPic(UserPic.DEFAULT) // If we have a URL and a profile key then set the user's profile picture
} else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey)) userConfig?.setPic(UserPic(url, profileKey))
}
} }
if (userConfig != null && userConfig.needsDump()) { if (userConfig != null && userConfig.needsDump()) {
configFactory.persist(userConfig, SnodeAPI.nowWithOffset) configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) 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) { if (displayName != null) {
binding.btnGroupNameDisplay.text = displayName binding.btnGroupNameDisplay.text = displayName
} }

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

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

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

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

@ -176,7 +176,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val kryo = Kryo() val kryo = Kryo()
kryo.isRegistrationRequired = false kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096) 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) kryo.writeClassAndObject(output, message)
output.close() output.close()
return Data.Builder() return Data.Builder()

@ -1,6 +1,7 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
@ -21,9 +22,9 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
override val maxFailureCount: Int = 1 override val maxFailureCount: Int = 1
val openGroupId: String? get() { 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 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" return "$server.$room"
} }

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

@ -14,7 +14,7 @@ interface Job {
// Keys used for database storage // Keys used for database storage
private val ID_KEY = "id" private val ID_KEY = "id"
private val FAILURE_COUNT_KEY = "failure_count" 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) suspend fun execute(dispatcherName: String)

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

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

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

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

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

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

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

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

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

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

@ -80,4 +80,7 @@
<string name="clearDevice">Clear Device</string> <string name="clearDevice">Clear Device</string>
<string name="clearDeviceOnly">Clear device only</string> <string name="clearDeviceOnly">Clear device only</string>
<string name="clearDeviceAndNetwork">Clear device and network</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> </resources>

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

Loading…
Cancel
Save