mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge branch 'dev' into open-group-invitations
This commit is contained in:
commit
11e223f5d8
@ -158,8 +158,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 162
|
||||
def canonicalVersionName = "1.10.3"
|
||||
def canonicalVersionCode = 163
|
||||
def canonicalVersionName = "1.10.4"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -254,6 +254,10 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.TextSecure.DayNight"/>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
@ -42,6 +42,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.snode.SnodeModule;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
@ -50,6 +51,7 @@ import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsignal.service.api.util.StreamDetails;
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.utilities.ThreadUtils;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
@ -68,7 +70,6 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
@ -91,8 +92,10 @@ import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.util.Date;
|
||||
@ -101,6 +104,7 @@ import java.util.Set;
|
||||
|
||||
import dagger.ObjectGraph;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlinx.coroutines.Job;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
|
||||
@ -173,8 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
MessagingModuleConfiguration.Companion.configure(this,
|
||||
DatabaseFactory.getStorage(this),
|
||||
DatabaseFactory.getAttachmentProvider(this),
|
||||
new SessionProtocolImpl(this));
|
||||
DatabaseFactory.getAttachmentProvider(this));
|
||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||
if (userPublicKey != null) {
|
||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
|
||||
@ -481,21 +484,31 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
|
||||
private void resubmitProfilePictureIfNeeded() {
|
||||
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
||||
// at a certain interval to ensure it's always available.
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
long now = new Date().getTime();
|
||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||
AsyncTask.execute(() -> {
|
||||
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this);
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey);
|
||||
ThreadUtils.queue(() -> {
|
||||
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
||||
try {
|
||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||
// Read the file into a byte array
|
||||
InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int count;
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
baos.write(buffer, 0, count);
|
||||
}
|
||||
baos.flush();
|
||||
byte[] profilePicture = baos.toByteArray();
|
||||
// Re-upload it
|
||||
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
||||
// Update the last profile picture upload date
|
||||
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
|
@ -108,28 +108,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return null // TODO: Implement
|
||||
}
|
||||
|
||||
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
||||
attachmentStream.contentType,
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachmentStream.length)),
|
||||
attachmentStream.preview,
|
||||
attachmentStream.width, attachmentStream.height,
|
||||
Optional.fromNullable(uploadResult.digest),
|
||||
attachmentStream.fileName,
|
||||
attachmentStream.voiceNote,
|
||||
attachmentStream.caption,
|
||||
uploadResult.url);
|
||||
attachmentStream.contentType,
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachmentStream.length)),
|
||||
attachmentStream.preview,
|
||||
attachmentStream.width, attachmentStream.height,
|
||||
Optional.fromNullable(uploadResult.digest),
|
||||
attachmentStream.fileName,
|
||||
attachmentStream.voiceNote,
|
||||
attachmentStream.caption,
|
||||
uploadResult.url);
|
||||
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
|
||||
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
|
||||
}
|
||||
|
||||
override fun updateAttachmentAfterUploadFailed(attachmentId: Long) {
|
||||
override fun handleFailedAttachmentUpload(attachmentId: Long) {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||
database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId)
|
||||
database.handleFailedAttachmentUpload(databaseAttachment.attachmentId)
|
||||
}
|
||||
|
||||
override fun getMessageID(serverID: Long): Long? {
|
||||
@ -230,23 +230,24 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac
|
||||
|
||||
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
|
||||
if (TextUtils.isEmpty(location)) { return null }
|
||||
if (TextUtils.isEmpty(key)) { return null }
|
||||
|
||||
// `key` can be empty in an open group context (no encryption means no encryption key)
|
||||
return try {
|
||||
val id: Long = location!!.toLong()
|
||||
val key: ByteArray = Base64.decode(key!!)
|
||||
SignalServiceAttachmentPointer(id,
|
||||
contentType,
|
||||
key,
|
||||
Optional.of(Util.toIntExact(size)),
|
||||
Optional.absent(),
|
||||
width,
|
||||
height,
|
||||
Optional.fromNullable(digest),
|
||||
Optional.fromNullable(fileName),
|
||||
isVoiceNote,
|
||||
Optional.fromNullable(caption),
|
||||
url)
|
||||
val id = location!!.toLong()
|
||||
val key = Base64.decode(key!!)
|
||||
SignalServiceAttachmentPointer(
|
||||
id,
|
||||
contentType,
|
||||
key,
|
||||
Optional.of(Util.toIntExact(size)),
|
||||
Optional.absent(),
|
||||
width,
|
||||
height,
|
||||
Optional.fromNullable(digest),
|
||||
Optional.fromNullable(fileName),
|
||||
isVoiceNote,
|
||||
Optional.fromNullable(caption),
|
||||
url
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -384,6 +384,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
||||
} else if (openGroupV2 != null) {
|
||||
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
||||
if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen")
|
||||
|| openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) {
|
||||
View openGroupGuidelinesView = findViewById(R.id.open_group_guidelines_view);
|
||||
openGroupGuidelinesView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
View rootView = findViewById(R.id.rootView);
|
||||
|
@ -546,7 +546,7 @@ public class ConversationFragment extends Fragment
|
||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||
.success(l -> {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||
if (l.contains(serverID)) {
|
||||
if (messageRecord.isMms()) {
|
||||
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
||||
@ -569,7 +569,7 @@ public class ConversationFragment extends Fragment
|
||||
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||
.success(l -> {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||
if (serverID != null && serverID.equals(serverId)) {
|
||||
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
|
||||
break;
|
||||
|
@ -393,7 +393,7 @@ public class AttachmentDatabase extends Database {
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
|
||||
}
|
||||
|
||||
public void updateAttachmentAfterUploadFailed(@NonNull AttachmentId id) {
|
||||
public void handleFailedAttachmentUpload(@NonNull AttachmentId id) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
|
@ -351,7 +351,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server)
|
||||
}
|
||||
|
||||
override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean {
|
||||
override fun isDuplicateMessage(timestamp: Long, sender: String): Boolean {
|
||||
return getReceivedMessageTimestamps().contains(timestamp)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.TextUtils;
|
||||
|
||||
@ -39,7 +38,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
|
||||
private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName();
|
||||
|
||||
private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024;
|
||||
private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxAttempts(3)
|
||||
.build(),
|
||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar);
|
||||
}
|
||||
|
||||
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
|
||||
super(parameters);
|
||||
|
||||
this.recipient = recipient;
|
||||
this.profileAvatar = profileAvatar;
|
||||
}
|
||||
@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar)
|
||||
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
|
||||
.build();
|
||||
return new Data.Builder()
|
||||
.putString(KEY_PROFILE_AVATAR, profileAvatar)
|
||||
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,7 +24,6 @@ import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
@ -315,23 +314,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
val threadID = thread.threadId
|
||||
val recipient = thread.recipient
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(this)
|
||||
val dialogMessage: String
|
||||
val message: String
|
||||
if (recipient.isGroupRecipient) {
|
||||
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
|
||||
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
|
||||
dialogMessage = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
||||
} else {
|
||||
dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
message = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||
}
|
||||
} else {
|
||||
dialogMessage = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||
message = resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setMessage(dialogMessage)
|
||||
dialog.setMessage(message)
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val context = this@HomeActivity as Context
|
||||
|
||||
// Cancel any outstanding jobs
|
||||
DatabaseFactory.getSessionJobDatabase(context).cancelPendingMessageSendJobs(threadID)
|
||||
// Send a leave group message if this is an active closed group
|
||||
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||
var isClosedGroup: Boolean
|
||||
@ -350,34 +350,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||
if (publicChat != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||
|
||||
OpenGroupAPI.leave(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(publicChat.server, publicChat.channel)
|
||||
} else if (openGroupV2 != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server)
|
||||
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(openGroupV2.server, openGroupV2.room)
|
||||
} else {
|
||||
threadDB.deleteConversation(threadID)
|
||||
}
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
// Delete the conversation
|
||||
val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||
if (v1OpenGroup != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
||||
apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server)
|
||||
OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server)
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(v1OpenGroup.server, v1OpenGroup.channel)
|
||||
} else if (v2OpenGroup != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||
apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(v2OpenGroup.server, v2OpenGroup.room)
|
||||
} else {
|
||||
threadDB.deleteConversation(threadID)
|
||||
}
|
||||
|
||||
// Update the badge count
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
// Notify the user
|
||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||
|
@ -10,8 +10,8 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.GridLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.*
|
||||
@ -42,9 +42,6 @@ import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
|
||||
import org.thoughtcrime.securesms.loki.viewmodel.State
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
|
||||
private val viewModel by viewModels<DefaultGroupsViewModel>()
|
||||
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
@ -83,23 +80,18 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
}
|
||||
|
||||
fun joinPublicChatIfPossible(url: String) {
|
||||
// add http if just an IP style / host style URL is entered but leave it if scheme is included
|
||||
val properString = if (!url.startsWith("http")) "http://$url" else url
|
||||
val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||
|
||||
val room = httpUrl.pathSegments().firstOrNull()
|
||||
val publicKey = httpUrl.queryParameter("public_key")
|
||||
// Add "http" if not entered explicitly
|
||||
val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url
|
||||
val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||
val room = url.pathSegments().firstOrNull()
|
||||
val publicKey = url.queryParameter("public_key")
|
||||
val isV2OpenGroup = !room.isNullOrEmpty()
|
||||
showLoader()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val (threadID, groupID) = if (isV2OpenGroup) {
|
||||
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
||||
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
||||
// non-standard port, add to server
|
||||
this.port(httpUrl.port())
|
||||
}
|
||||
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply {
|
||||
if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server
|
||||
}.build()
|
||||
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||
@ -107,21 +99,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
threadID to groupID
|
||||
} else {
|
||||
val channel: Long = 1
|
||||
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel)
|
||||
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, stringWithExplicitScheme, channel)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||
threadID to groupID
|
||||
}
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// go to the new conversation and finish this one
|
||||
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false))
|
||||
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
|
||||
openConversationActivity(this@JoinPublicChatActivity, threadID, recipient)
|
||||
finish()
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("JoinPublicChatActivity", "Failed to join open group.", e)
|
||||
Log.e("Loki", "Couldn't join open group.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoader()
|
||||
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
@ -175,19 +165,40 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
||||
|
||||
// region Enter Chat URL Fragment
|
||||
class EnterChatURLFragment : Fragment() {
|
||||
|
||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
defaultRoomsContainer.isVisible = state is State.Success
|
||||
defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// TODO: Show a loader
|
||||
}
|
||||
is State.Error -> {
|
||||
// TODO: Hide the loader
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||
defaultRoomsGridLayout.removeAllViews()
|
||||
defaultRoomsGridLayout.useDefaultMargins = false
|
||||
groups.forEach { defaultGroup ->
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip
|
||||
val drawable = defaultGroup.image?.let { bytes ->
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
||||
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||
isCircular = true
|
||||
}
|
||||
@ -197,35 +208,14 @@ class EnterChatURLFragment : Fragment() {
|
||||
chip.setOnClickListener {
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||
}
|
||||
|
||||
defaultRoomsGridLayout.addView(chip)
|
||||
}
|
||||
if (groups.size and 1 != 0) {
|
||||
// add a filler weight 1 view
|
||||
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
defaultRoomsParent.isVisible = state is State.Success
|
||||
defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// show a loader here probs
|
||||
}
|
||||
is State.Error -> {
|
||||
// hide the loader and the
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
|
||||
class OpenGroupGuidelinesActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_group_guidelines)
|
||||
communityGuidelinesTextView.text = """
|
||||
In order for our open group to be a fun environment, full of robust and constructive discussion, please follow these four simple rules:
|
||||
|
||||
1. Keep conversations on-topic and add value to the discussion (no referral links, spamming, or off-topic discussion).
|
||||
|
||||
2. You don't have to love everyone, but be civil (no baiting, excessively partisan arguments, threats, and so on; use common sense).
|
||||
|
||||
3. Do not be a shill. Comparison and criticism is reasonable, but blatant shilling is not.
|
||||
|
||||
4. Don't post explicit content, be it excessive offensive language, or content which is sexual or violent in nature.
|
||||
|
||||
If you break these rules, you’ll be warned by an admin. If your behaviour doesn’t improve, you will be removed from the open group.
|
||||
|
||||
If you see or experience any destructive behaviour, please contact an admin.
|
||||
|
||||
——————————
|
||||
|
||||
SCAMMER WARNING
|
||||
|
||||
Trust only those with an admin tag in the chat. No admin will ever DM you first. No admin will ever message you for Oxen coins.
|
||||
""".trimIndent()
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -23,18 +23,15 @@ import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.ProfilePictureUtilities
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
@ -51,7 +48,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
@ -127,7 +123,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
||||
if (resultCode != Activity.RESULT_OK) { return }
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return
|
||||
}
|
||||
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
||||
var inputFile: Uri? = data?.data
|
||||
if (inputFile == null && tempFile != null) {
|
||||
@ -136,7 +134,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
||||
}
|
||||
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
||||
if (resultCode != Activity.RESULT_OK) { return }
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return
|
||||
}
|
||||
AsyncTask.execute {
|
||||
try {
|
||||
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
||||
@ -186,42 +186,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
val profilePicture = profilePictureToBeUploaded
|
||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
val storageAPI = FileServerAPI.shared
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
AsyncTask.execute {
|
||||
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
|
||||
val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time)
|
||||
}
|
||||
TextSecurePreferences.setProfilePictureURL(this, url)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
promises.add(deferred.promise)
|
||||
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
|
||||
}
|
||||
|
||||
all(promises).bind {
|
||||
// updating the profile name or picture
|
||||
if (profilePicture != null || displayName != null) {
|
||||
task {
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
||||
}
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
}
|
||||
} else {
|
||||
Promise.of(Unit)
|
||||
val compoundPromise = all(promises)
|
||||
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
||||
}
|
||||
}.alwaysUi {
|
||||
if (profilePicture != null || displayName != null) {
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
}
|
||||
}
|
||||
compoundPromise.alwaysUi {
|
||||
if (displayName != null) {
|
||||
btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
profilePictureView.recycle() // clear cached image before update tje profilePictureView
|
||||
profilePictureView.recycle() // Clear the cached image before updating
|
||||
profilePictureView.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
|
@ -11,6 +11,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
@ -138,9 +139,10 @@ class PublicChatManager(private val context: Context) {
|
||||
val groupId = OpenGroup.getId(channel, server)
|
||||
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
||||
GroupManager.deleteGroup(groupAddress, context)
|
||||
|
||||
Util.runOnMain { startPollersIfNeeded() }
|
||||
ThreadUtils.queue {
|
||||
GroupManager.deleteGroup(groupAddress, context) // Must be invoked on a background thread
|
||||
Util.runOnMain { startPollersIfNeeded() }
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChat(server: String, room: String) {
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
@ -98,11 +99,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
|
||||
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val contentValues = ContentValues(2)
|
||||
val contentValues = ContentValues(3)
|
||||
contentValues.put(Companion.messageID, messageID)
|
||||
contentValues.put(Companion.serverID, serverID)
|
||||
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
|
||||
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||
database.insertWithOnConflict(messageIDTable, null, contentValues, CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
fun getOriginalThreadID(messageID: Long): Long {
|
||||
@ -114,11 +115,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
|
||||
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val contentValues = ContentValues(2)
|
||||
val contentValues = ContentValues(3)
|
||||
contentValues.put(Companion.messageID, messageID)
|
||||
contentValues.put(Companion.serverID, serverID)
|
||||
contentValues.put(Companion.threadID, threadID)
|
||||
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||
database.insertWithOnConflict(messageThreadMappingTable, null, contentValues, CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
fun getErrorMessage(messageID: Long): String? {
|
||||
|
@ -71,6 +71,30 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPendingMessageSendJobs(threadID: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val attachmentUploadJobKeys = mutableListOf<String>()
|
||||
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
|
||||
val job = jobFromCursor(cursor) as AttachmentUploadJob?
|
||||
if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) }
|
||||
}
|
||||
val messageSendJobKeys = mutableListOf<String>()
|
||||
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( MessageSendJob.KEY )) { cursor ->
|
||||
val job = jobFromCursor(cursor) as MessageSendJob?
|
||||
if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) }
|
||||
}
|
||||
if (attachmentUploadJobKeys.isNotEmpty()) {
|
||||
val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ")
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
|
||||
arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString ))
|
||||
}
|
||||
if (messageSendJobKeys.isNotEmpty()) {
|
||||
val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ")
|
||||
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
|
||||
arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString ))
|
||||
}
|
||||
}
|
||||
|
||||
fun isJobCanceled(job: Job): Boolean {
|
||||
val database = databaseHelper.readableDatabase
|
||||
var cursor: android.database.Cursor? = null
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.sending_receiving.*
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
@ -15,12 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
|
||||
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
@ -195,7 +191,7 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
@ -330,7 +326,7 @@ object ClosedGroupsProtocolV2 {
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
|
||||
val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first
|
||||
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
|
@ -35,11 +35,10 @@ object OpenGroupUtilities {
|
||||
val groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
|
||||
val application = ApplicationContext.getInstance(context)
|
||||
|
||||
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
||||
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.removeLastDeletionServerId(room, server)
|
||||
storage.removeLastMessageServerId(room, server)
|
||||
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
||||
|
||||
return group
|
||||
}
|
||||
|
@ -20,5 +20,4 @@ class DefaultGroupsViewModel : ViewModel() {
|
||||
}.onStart {
|
||||
emit(State.Loading)
|
||||
}.asLiveData()
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
|
||||
class OpenGroupGuidelinesView : FrameLayout {
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null)
|
||||
addView(contentView)
|
||||
readButton.setOnClickListener {
|
||||
val activity = context as ConversationActivity
|
||||
val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java)
|
||||
activity.push(intent)
|
||||
}
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
@ -94,7 +94,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
android:text="Session will occasionally check for new messages in the background." />
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
|
@ -33,23 +33,27 @@
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:id="@+id/defaultRoomsParent"
|
||||
android:id="@+id/defaultRoomsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_marginVertical="16dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="@string/activity_join_public_chat_join_rooms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/defaultRoomsGridLayout"
|
||||
android:columnCount="2"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
36
app/src/main/res/layout/activity_open_group_guidelines.xml
Normal file
36
app/src/main/res/layout/activity_open_group_guidelines.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:textSize="@dimen/large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Community Guidelines" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/communityGuidelinesTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginBottom="@dimen/large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
@ -56,7 +56,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
@ -94,7 +94,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
android:text="Session will occasionally check for new messages in the background." />
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
|
@ -134,6 +134,12 @@
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.OpenGroupGuidelinesView
|
||||
android:id="@+id/open_group_guidelines_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_content"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1,14 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.chip.Chip
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight"
|
||||
style="?attr/chipStyle"
|
||||
app:chipStartPadding="6dp"
|
||||
app:chipStartPadding="4dp"
|
||||
app:chipBackgroundColor="@color/open_group_chip_color"
|
||||
android:layout_columnWeight="1"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
tools:text="Main Group"
|
||||
android:ellipsize="end"
|
||||
tools:layout_width="wrap_content"
|
||||
app:chipMinTouchTargetSize="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp" />
|
||||
android:layout_height="wrap_content" />
|
@ -33,23 +33,27 @@
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:id="@+id/defaultRoomsParent"
|
||||
android:id="@+id/defaultRoomsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_marginVertical="16dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="@string/activity_join_public_chat_join_rooms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/defaultRoomsGridLayout"
|
||||
android:columnCount="2"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
72
app/src/main/res/layout/view_open_group_guidelines.xml
Normal file
72
app/src/main/res/layout/view_open_group_guidelines.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/cell_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="@dimen/small_spacing"
|
||||
android:paddingBottom="@dimen/small_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="@dimen/small_spacing"
|
||||
android:background="@color/accent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="bold"
|
||||
android:text="Pinned message" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:maxWidth="260dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:maxLines="2"
|
||||
android:text="Community guidelines" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="@dimen/small_spacing" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/readButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
android:layout_marginRight="12dp"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textStyle="normal"
|
||||
android:text="Read" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal" />
|
||||
|
||||
</LinearLayout>
|
@ -22,6 +22,7 @@
|
||||
<color name="new_conversation_button_collapsed_background">#F5F5F5</color>
|
||||
<color name="pn_option_background">#FCFCFC</color>
|
||||
<color name="fake_chat_bubble_background">#F5F5F5</color>
|
||||
<color name="open_group_chip_color">#0D000000</color>
|
||||
|
||||
|
||||
<color name="default_background_start">#ffffff</color>
|
||||
|
@ -31,6 +31,7 @@
|
||||
<color name="pn_option_background">#1B1B1B</color>
|
||||
<color name="pn_option_border">#212121</color>
|
||||
<color name="paths_building">#FFCE3A</color>
|
||||
<color name="open_group_chip_color">#0DFFFFFF</color>
|
||||
|
||||
<array name="profile_picture_placeholder_colors">
|
||||
<item>#5ff8b0</item>
|
||||
|
@ -27,7 +27,7 @@
|
||||
android:dependency="pref_key_enable_notifications"
|
||||
android:key="pref_key_use_fcm"
|
||||
android:title="Use Fast Mode"
|
||||
android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google."
|
||||
android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers."
|
||||
android:defaultValue="false" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
@ -29,8 +29,8 @@ interface MessageDataProvider {
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||
|
@ -2,13 +2,11 @@ package org.session.libsession.messaging
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
|
||||
class MessagingModuleConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val sessionProtocol: SessionProtocol
|
||||
val messageDataProvider: MessageDataProvider
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@ -16,11 +14,10 @@ class MessagingModuleConfiguration(
|
||||
|
||||
fun configure(context: Context,
|
||||
storage: StorageProtocol,
|
||||
messageDataProvider: MessageDataProvider,
|
||||
sessionProtocol: SessionProtocol
|
||||
messageDataProvider: MessageDataProvider
|
||||
) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol)
|
||||
shared = MessagingModuleConfiguration(context, storage, messageDataProvider)
|
||||
}
|
||||
}
|
||||
}
|
@ -92,7 +92,7 @@ interface StorageProtocol {
|
||||
fun removeLastDeletionServerId(room: String, server: String)
|
||||
|
||||
// Message Handling
|
||||
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
||||
fun isDuplicateMessage(timestamp: Long, sender: String): Boolean
|
||||
fun getReceivedMessageTimestamps(): Set<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||
|
@ -15,8 +15,8 @@ import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object FileServerAPIV2 {
|
||||
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||
private const val SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
const val SERVER = "http://88.99.175.227"
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
@ -43,7 +43,7 @@ object FileServerAPIV2 {
|
||||
}
|
||||
|
||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||
val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||
val url = HttpUrl.parse(SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
@ -64,7 +64,7 @@ object FileServerAPIV2 {
|
||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||
}
|
||||
if (request.useOnionRouting) {
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e ->
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), SERVER, SERVER_PUBLIC_KEY).fail { e ->
|
||||
Log.e("Loki", "File server request failed.", e)
|
||||
}
|
||||
} else {
|
||||
|
@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
||||
if (exception == Error.NoAttachment) {
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
||||
// got a "Cannot GET ..." error from the file server.
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else {
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||
val tempFile = createTempFile()
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
||||
val stream = if (openGroupV2 == null) {
|
||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
|
||||
val inputStream = if (openGroupV2 == null) {
|
||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||
@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
} else {
|
||||
val url = HttpUrl.parse(attachment.url)!!
|
||||
val fileId = url.pathSegments().last()
|
||||
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||
val fileID = url.pathSegments().last()
|
||||
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||
tempFile.writeBytes(it)
|
||||
}
|
||||
FileInputStream(tempFile)
|
||||
}
|
||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
|
||||
tempFile.delete()
|
||||
handleSuccess()
|
||||
} catch (e: Exception) {
|
||||
|
@ -3,8 +3,12 @@ package org.session.libsession.messaging.jobs
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okhttp3.MultipartBody
|
||||
import okio.Buffer
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
@ -15,9 +19,11 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.service.internal.crypto.PaddingInputStream
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.util.*
|
||||
|
||||
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
@ -45,27 +51,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
|
||||
override fun execute() {
|
||||
try {
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
val usePadding = false
|
||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server
|
||||
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
||||
val attachmentKey = Util.getSecretBytes(64)
|
||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||
val uploadResult = if (openGroupV2 != null) {
|
||||
val dataBytes = attachmentData.data.readBytes()
|
||||
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
||||
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
||||
} else {
|
||||
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
val v2OpenGroup = storage.getV2OpenGroup(threadID)
|
||||
val v1OpenGroup = storage.getOpenGroup(threadID)
|
||||
if (v2OpenGroup != null) {
|
||||
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
||||
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else if (v1OpenGroup == null) {
|
||||
val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
|
||||
FileServerAPIV2.upload(it)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else { // V1 open group
|
||||
val server = v1OpenGroup.server
|
||||
val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
|
||||
attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
|
||||
val result = FileServerAPI.shared.uploadAttachment(server, pushData)
|
||||
handleSuccess(attachment, ByteArray(0), result)
|
||||
}
|
||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
@ -77,17 +85,49 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
}
|
||||
|
||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
|
||||
// Key
|
||||
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
|
||||
// Length
|
||||
val rawLength = attachment.length
|
||||
val length = if (encrypt) {
|
||||
val paddedLength = PaddingInputStream.getPaddedSize(rawLength)
|
||||
AttachmentCipherOutputStream.getCiphertextLength(paddedLength)
|
||||
} else {
|
||||
attachment.length
|
||||
}
|
||||
// In & out streams
|
||||
// PaddingInputStream adds padding as data is read out from it. AttachmentCipherOutputStream
|
||||
// encrypts as it writes data.
|
||||
val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream
|
||||
val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory()
|
||||
// Create a digesting request body but immediately read it out to a buffer. Doing this makes
|
||||
// it easier to deal with inputStream and outputStreamFactory.
|
||||
val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener)
|
||||
val contentType = "application/octet-stream"
|
||||
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, pad.listener)
|
||||
Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.")
|
||||
val b = Buffer()
|
||||
drb.writeTo(b)
|
||||
val data = b.readByteArray()
|
||||
// Upload the data
|
||||
val id = upload(data).get()
|
||||
val digest = drb.transmittedDigest
|
||||
// Return
|
||||
return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest))
|
||||
}
|
||||
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
Log.d(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
|
@ -130,12 +130,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
// Message send jobs waiting for the attachment to upload
|
||||
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
||||
val retryInterval: Long = 1000 * 4
|
||||
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
|
||||
timer.schedule(delay = retryInterval) {
|
||||
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||
queue.offer(job)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Regular job failure
|
||||
|
@ -348,7 +348,7 @@ object OpenGroupAPIV2 {
|
||||
|
||||
// region General
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||
fun compactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val requests = rooms.mapNotNull { room ->
|
||||
|
@ -1,26 +1,29 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
object MessageDecrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
/**
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
* @param ciphertext the data to decrypt.
|
||||
* @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
|
||||
*
|
||||
* @return the padded plaintext.
|
||||
*/
|
||||
public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
val signatureSize = Sign.BYTES
|
||||
@ -32,9 +35,9 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.DecryptionFailed
|
||||
throw MessageReceiver.Error.DecryptionFailed
|
||||
}
|
||||
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw SessionProtocol.Exception.DecryptionFailed }
|
||||
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw MessageReceiver.Error.DecryptionFailed }
|
||||
// 2. ) Get the message parts
|
||||
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
||||
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
||||
@ -43,10 +46,10 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
||||
try {
|
||||
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
||||
if (!isValid) { throw SessionProtocol.Exception.InvalidSignature }
|
||||
if (!isValid) { throw MessageReceiver.Error.InvalidSignature }
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
|
||||
throw SessionProtocol.Exception.InvalidSignature
|
||||
throw MessageReceiver.Error.InvalidSignature
|
||||
}
|
||||
// 4. ) Get the sender's X25519 public key
|
||||
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object MessageSenderEncryption {
|
||||
object MessageEncrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
@ -25,7 +25,7 @@ object MessageSenderEncryption {
|
||||
*
|
||||
* @return the encrypted message.
|
||||
*/
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
@ -10,35 +10,24 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
object MessageReceiver {
|
||||
|
||||
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
||||
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
internal sealed class Error(message: String) : Exception(message) {
|
||||
object DuplicateMessage: Error("Duplicate message.")
|
||||
object InvalidMessage: Error("Invalid message.")
|
||||
object UnknownMessage: Error("Unknown message type.")
|
||||
object UnknownEnvelopeType: Error("Unknown envelope type.")
|
||||
object NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.")
|
||||
object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.")
|
||||
object DecryptionFailed : Exception("Couldn't decrypt message.")
|
||||
object InvalidSignature: Error("Invalid message signature.")
|
||||
object NoData: Error("Received an empty envelope.")
|
||||
object SenderBlocked: Error("Received a message from a blocked user.")
|
||||
object NoThread: Error("Couldn't find thread for message.")
|
||||
object SelfSend: Error("Message addressed at self.")
|
||||
object ParsingFailed : Error("Couldn't parse ciphertext message.")
|
||||
// Shared sender keys
|
||||
object InvalidGroupPublicKey: Error("Invalid group public key.")
|
||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is DuplicateMessage -> false
|
||||
is InvalidMessage -> false
|
||||
is UnknownMessage -> false
|
||||
is UnknownEnvelopeType -> false
|
||||
is InvalidSignature -> false
|
||||
is NoData -> false
|
||||
is NoThread -> false
|
||||
is SenderBlocked -> false
|
||||
is SelfSend -> false
|
||||
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||
is UnknownEnvelopeType, is InvalidSignature, is NoData,
|
||||
is SenderBlocked, is SelfSend -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@ -46,13 +35,15 @@ object MessageReceiver {
|
||||
internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val isOpenGroupMessage = openGroupServerID != null
|
||||
val isOpenGroupMessage = (openGroupServerID != null)
|
||||
// Parse the envelope
|
||||
val envelope = SignalServiceProtos.Envelope.parseFrom(data)
|
||||
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
|
||||
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
|
||||
// for this issue.
|
||||
if (storage.isMessageDuplicated(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) throw Error.DuplicateMessage
|
||||
if (storage.isDuplicateMessage(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) {
|
||||
throw Error.DuplicateMessage
|
||||
}
|
||||
// Decrypt the contents
|
||||
val ciphertext = envelope.content ?: throw Error.NoData
|
||||
var plaintext: ByteArray? = null
|
||||
@ -65,7 +56,7 @@ object MessageReceiver {
|
||||
when (envelope.type) {
|
||||
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
||||
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair)
|
||||
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
|
||||
plaintext = decryptionResult.first
|
||||
sender = decryptionResult.second
|
||||
}
|
||||
@ -81,7 +72,7 @@ object MessageReceiver {
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
fun decrypt() {
|
||||
try {
|
||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), encryptionKeyPair)
|
||||
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), encryptionKeyPair)
|
||||
plaintext = decryptionResult.first
|
||||
sender = decryptionResult.second
|
||||
} catch (e: Exception) {
|
||||
@ -100,9 +91,9 @@ object MessageReceiver {
|
||||
}
|
||||
}
|
||||
// Don't process the envelope any further if the message has been handled already
|
||||
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
||||
if (storage.isDuplicateMessage(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
||||
// Don't process the envelope any further if the sender is blocked
|
||||
if (isBlock(sender!!)) throw Error.SenderBlocked
|
||||
if (isBlocked(sender!!)) throw Error.SenderBlocked
|
||||
// Parse the proto
|
||||
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
|
||||
// Parse the message
|
||||
@ -113,7 +104,7 @@ object MessageReceiver {
|
||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||
ConfigurationMessage.fromProto(proto) ?:
|
||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||
// Ignore self sends if needed
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
||||
// Guard against control messages in open groups
|
||||
if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage
|
||||
|
@ -1,11 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object MessageReceiverDecryption {
|
||||
|
||||
internal fun decryptWithSessionProtocol(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
return MessagingModuleConfiguration.shared.sessionProtocol.decrypt(ciphertext, x25519KeyPair)
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.*
|
||||
import org.session.libsession.messaging.open_groups.*
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.RawResponsePromise
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
@ -27,6 +26,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.lang.IllegalStateException
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
|
||||
@ -37,8 +37,6 @@ object MessageSender {
|
||||
sealed class Error(val description: String) : Exception(description) {
|
||||
object InvalidMessage : Error("Invalid message.")
|
||||
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
|
||||
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
|
||||
object NoUserX25519KeyPair : Error("Couldn't find user X25519 key pair.")
|
||||
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||
object SigningFailed : Error("Couldn't sign message.")
|
||||
object EncryptionFailed : Error("Couldn't encrypt message.")
|
||||
@ -46,17 +44,10 @@ object MessageSender {
|
||||
// Closed groups
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key.")
|
||||
object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
|
||||
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
|
||||
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
||||
|
||||
// Precondition
|
||||
class PreconditionFailure(val reason: String): Error(reason)
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is InvalidMessage -> false
|
||||
is ProtoConversionFailed -> false
|
||||
is ProofOfWorkCalculationFailed -> false
|
||||
is InvalidClosedGroupUpdate -> false
|
||||
is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@ -76,7 +67,9 @@ object MessageSender {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
message.sender = userPublicKey
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
@ -91,8 +84,7 @@ object MessageSender {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
@ -125,13 +117,12 @@ object MessageSender {
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext: ByteArray
|
||||
when (destination) {
|
||||
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey)
|
||||
is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
@ -145,8 +136,7 @@ object MessageSender {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Send the result
|
||||
@ -201,7 +191,9 @@ object MessageSender {
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
message.sender = storage.getUserPublicKey()
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
@ -210,18 +202,15 @@ object MessageSender {
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
||||
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
||||
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
||||
is Destination.OpenGroup -> {
|
||||
message.recipient = "${destination.server}.${destination.channel}"
|
||||
val server = destination.server
|
||||
val channel = destination.channel
|
||||
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
|
||||
// Convert the message to an open group message
|
||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
||||
throw Error.InvalidMessage
|
||||
@ -239,7 +228,6 @@ object MessageSender {
|
||||
message.recipient = "${destination.server}.${destination.room}"
|
||||
val server = destination.server
|
||||
val room = destination.room
|
||||
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
val displayName = storage.getUserDisplayName()!!
|
||||
@ -251,20 +239,17 @@ object MessageSender {
|
||||
message.profile = Profile(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
|
||||
val proto = message.toProto()!!
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
val openGroupMessage = OpenGroupMessageV2(
|
||||
sender = message.sender,
|
||||
sentTimestamp = message.sentTimestamp!!,
|
||||
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||
sender = message.sender,
|
||||
sentTimestamp = message.sentTimestamp!!,
|
||||
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||
)
|
||||
|
||||
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||
message.openGroupServerMessageID = it.serverID
|
||||
handleSuccessfulMessageSend(message, destination)
|
||||
@ -272,7 +257,6 @@ object MessageSender {
|
||||
}.fail {
|
||||
handleFailure(it)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
@ -285,7 +269,7 @@ object MessageSender {
|
||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
||||
val messageID = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
||||
// Ignore future self-sends
|
||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||
// Track the open group server message ID
|
||||
@ -293,7 +277,7 @@ object MessageSender {
|
||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||
if (threadID != null && threadID >= 0) {
|
||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
}
|
||||
}
|
||||
// Mark the message as sent
|
||||
@ -323,16 +307,16 @@ object MessageSender {
|
||||
// Convenience
|
||||
@JvmStatic
|
||||
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachmentIDs = messageDataProvider.getAttachmentIDsFor(message.id!!)
|
||||
message.attachmentIDs.addAll(attachmentIDs)
|
||||
message.quote = Quote.from(quote)
|
||||
message.linkPreview = LinkPreview.from(linkPreview)
|
||||
message.linkPreview?.let {
|
||||
if (it.attachmentID == null) {
|
||||
dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let {
|
||||
message.linkPreview!!.attachmentID = it
|
||||
message.attachmentIDs.remove(it)
|
||||
message.linkPreview?.let { linkPreview ->
|
||||
if (linkPreview.attachmentID == null) {
|
||||
messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
|
||||
message.linkPreview!!.attachmentID = attachmentID
|
||||
message.attachmentIDs.remove(attachmentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
const val groupSizeLimit = 100
|
||||
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
@ -45,7 +46,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@ -179,13 +180,11 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Update the zombie list
|
||||
val oldZombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
|
||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
@ -194,17 +193,14 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
|
||||
// Send the new encryption key pair to the remaining group members
|
||||
// At this stage we know the user is admin, no need to test
|
||||
// Send the new encryption key pair to the remaining group members.
|
||||
// At this stage we know the user is admin, no need to test.
|
||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||
// Notify the user
|
||||
|
||||
// Insert an outgoing notification
|
||||
// we don't display zombie members in the notification as users have already been notified when those members left
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = membersToRemove.minus(oldZombies)
|
||||
if (notificationMembers.isNotEmpty()) {
|
||||
// no notification to display when only zombies have been removed
|
||||
// No notification to display when only zombies have been removed
|
||||
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime)
|
||||
@ -259,16 +255,16 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
// Replace call will not succeed if no value already set
|
||||
pendingKeyPairs.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Make sure we set the pending key pair or wait until it is not null
|
||||
} while (!pendingKeyPairs.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
pendingKeyPairs[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,7 +275,7 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.map { publicKey ->
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
@ -307,14 +303,14 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
||||
return
|
||||
}
|
||||
// Get the latest encryption key pair
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
// Send it
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||
|
@ -35,7 +35,7 @@ import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
internal fun MessageReceiver.isBlock(publicKey: String): Boolean {
|
||||
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||
return recipient.isBlocked
|
||||
@ -96,12 +96,9 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
|
||||
}
|
||||
}
|
||||
|
||||
// Data Extraction Notification handling
|
||||
|
||||
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
||||
// we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too)
|
||||
// We don't handle data extraction messages for groups (they shouldn't be sent, but just in case we filter them here too)
|
||||
if (message.groupPublicKey != null) return
|
||||
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender!!
|
||||
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
||||
@ -112,20 +109,20 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
|
||||
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Configuration message handling
|
||||
|
||||
private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)
|
||||
&& !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closeGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
for (closedGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
@ -137,14 +134,12 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||
storage.setDisplayName(userPublicKey, message.displayName)
|
||||
}
|
||||
if (message.profileKey.isNotEmpty()) {
|
||||
if (message.profileKey.isNotEmpty() && !message.profilePicture.isNullOrEmpty()
|
||||
&& TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||
// handle profile photo
|
||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
}
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
}
|
||||
@ -159,41 +154,32 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
|
||||
// Get or create thread
|
||||
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
|
||||
// exist. This is intentional, but it's very non-obvious.
|
||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||
|
||||
if (threadID < 0) {
|
||||
// thread doesn't exist, should only be reached in a case where we are processing open group messages for no longer existent thread
|
||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||
throw MessageReceiver.Error.NoThread
|
||||
}
|
||||
|
||||
val openGroup = threadID.let {
|
||||
storage.getOpenGroup(it.toString())
|
||||
}
|
||||
|
||||
// Update profile if needed
|
||||
val newProfile = message.profile
|
||||
|
||||
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
|
||||
val profile = message.profile
|
||||
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||
val displayName = newProfile.displayName!!
|
||||
val displayName = profile.displayName!!
|
||||
if (displayName.isNotEmpty()) {
|
||||
profileManager.setDisplayName(context, recipient, displayName)
|
||||
}
|
||||
if (newProfile.profileKey?.isNotEmpty() == true
|
||||
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) {
|
||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
||||
if (profile.profileKey?.isNotEmpty() == true && profile.profilePictureURL?.isNotEmpty() == true
|
||||
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profile.profileKey))) {
|
||||
profileManager.setProfileKey(context, recipient, profile.profileKey!!)
|
||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||
val newUrl = newProfile.profilePictureURL
|
||||
if (!newUrl.isNullOrEmpty()) {
|
||||
profileManager.setProfilePictureURL(context, recipient, newUrl)
|
||||
if (userPublicKey == message.sender) {
|
||||
profileManager.updateOpenGroupProfilePicturesIfNeeded(context)
|
||||
}
|
||||
}
|
||||
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
|
||||
}
|
||||
}
|
||||
// Parse quote if needed
|
||||
@ -201,10 +187,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||
val quote = proto.dataMessage.quote
|
||||
val author = Address.fromSerialized(quote.author)
|
||||
val messageInfo = MessagingModuleConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
if (messageInfo != null) {
|
||||
val attachments = if (messageInfo.second) MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
quoteModel = QuoteModel(quote.id, author, MessagingModuleConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
quoteModel = QuoteModel(quote.id, author, messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||
} else {
|
||||
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
||||
}
|
||||
@ -225,6 +212,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse attachments if needed
|
||||
val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
|
||||
val attachment = Attachment.fromProto(proto)
|
||||
if (!attachment.isValid()) {
|
||||
@ -233,7 +221,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
return@mapNotNull attachment
|
||||
}
|
||||
}
|
||||
// Parse stickers if needed
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
||||
@ -247,7 +234,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
}
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !message.isMediaMessage())
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
}
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
@ -278,7 +266,6 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Parameter @sender:String is just for inserting incoming info message
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@ -290,11 +277,10 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
@ -322,11 +308,11 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
// Unwrap the message
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair for inactive group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
||||
@ -336,7 +322,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
||||
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
||||
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
@ -347,7 +333,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
return
|
||||
}
|
||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
Log.d("Loki", "Received a new closed group encryption key pair.")
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
|
||||
@ -360,11 +346,11 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
@ -375,10 +361,8 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@ -395,11 +379,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
@ -411,19 +395,28 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
val newMembers = members + updateMembers
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
// Send the latest encryption key pair to the added members if the current user is the admin of the group
|
||||
//
|
||||
// This fixes a race condition where:
|
||||
// • A member removes another member.
|
||||
// • A member adds someone to the group and sends them the latest group key pair.
|
||||
// • The admin is offline during all of this.
|
||||
// • When the admin comes back online they see the member removed message and generate + distribute a new key pair,
|
||||
// but they don't know about the added member yet.
|
||||
// • Now they see the member added message.
|
||||
//
|
||||
// Without the code below, the added member(s) would never get the key pair that was generated by the admin when they saw
|
||||
// the member removed message.
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
@ -448,65 +441,54 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group.")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
|
||||
val removedMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
// Check that the admin wasn't removed
|
||||
if (updateMembers.contains(admins.first())) {
|
||||
if (removedMembers.contains(admins.first())) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the message was sent by the group admin
|
||||
if (!admins.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in updateMembers }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members - updateMembers
|
||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
||||
val senderLeft = senderPublicKey in updateMembers
|
||||
// If the admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in removedMembers }
|
||||
val newMembers = members - removedMembers
|
||||
// A user should be posting a MEMBERS_LEFT in case they leave, so this shouldn't be encountered
|
||||
val senderLeft = senderPublicKey in removedMembers
|
||||
if (senderLeft) {
|
||||
android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
|
||||
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in updateMembers
|
||||
|
||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
||||
val wasCurrentUserRemoved = userPublicKey in removedMembers
|
||||
// Admin should send a MEMBERS_LEFT message but handled here just in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// update zombie members
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
|
||||
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
|
||||
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
// Notify the user
|
||||
// we don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = updateMembers.minus(zombies)
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = removedMembers.minus(zombies)
|
||||
if (notificationMembers.isNotEmpty()) {
|
||||
// no notification to display when only zombies have been removed
|
||||
// No notification to display when only zombies have been removed
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@ -528,11 +510,11 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
@ -546,19 +528,16 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
val userLeft = (userPublicKey == senderPublicKey)
|
||||
|
||||
if (didAdminLeave || userLeft) {
|
||||
// admin left the group of linked device left the group
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// update zombie members
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
if (userLeft) {
|
||||
//sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@ -566,9 +545,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
sentTimestamp: Long,
|
||||
senderPublicKey: String): Boolean {
|
||||
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.formationTimestamp > sentTimestamp) {
|
||||
|
@ -70,11 +70,11 @@ class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val e
|
||||
isPollOngoing = true
|
||||
val server = openGroups.first().server // assume all the same server
|
||||
val rooms = openGroups.map { it.room }
|
||||
return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results ->
|
||||
return OpenGroupAPIV2.compactPoll(rooms = rooms, server).successBackground { results ->
|
||||
results.forEach { (room, results) ->
|
||||
val serverRoomId = "$server.$room"
|
||||
handleDeletedMessages(serverRoomId,results.deletions)
|
||||
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
||||
handleDeletedMessages(serverRoomId,results.deletions)
|
||||
}
|
||||
}.always {
|
||||
isPollOngoing = false
|
||||
@ -120,7 +120,11 @@ class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val e
|
||||
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
|
||||
messagingModule.messageDataProvider.getMessageID(serverId, threadId)
|
||||
val id = messagingModule.messageDataProvider.getMessageID(serverId, threadId)
|
||||
if (id == null) {
|
||||
Log.d("Loki", "Couldn't find server ID $serverId")
|
||||
}
|
||||
id
|
||||
}
|
||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||
|
@ -12,16 +12,15 @@ import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@WorkerThread
|
||||
internal object AESGCM {
|
||||
|
||||
internal data class EncryptionResult(
|
||||
internal val ciphertext: ByteArray,
|
||||
internal val symmetricKey: ByteArray,
|
||||
internal val ephemeralPublicKey: ByteArray
|
||||
)
|
||||
|
||||
internal val gcmTagSize = 128
|
||||
internal val ivSize = 12
|
||||
|
||||
internal data class EncryptionResult(
|
||||
internal val ciphertext: ByteArray,
|
||||
internal val symmetricKey: ByteArray,
|
||||
internal val ephemeralPublicKey: ByteArray
|
||||
)
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
|
@ -5,7 +5,6 @@ import okhttp3.Request
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
@ -42,7 +41,7 @@ object DownloadUtilities {
|
||||
@JvmStatic
|
||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
|
||||
if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) {
|
||||
if (url.contains(FileServerAPIV2.SERVER)) {
|
||||
val httpUrl = HttpUrl.parse(url)!!
|
||||
val fileId = httpUrl.pathSegments().last()
|
||||
try {
|
||||
|
@ -0,0 +1,46 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import okio.Buffer
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
||||
import org.session.libsignal.service.internal.push.ProfileAvatarData
|
||||
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
|
||||
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.*
|
||||
|
||||
object ProfilePictureUtilities {
|
||||
|
||||
fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val inputStream = ByteArrayInputStream(profilePicture)
|
||||
val outputStream = ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong())
|
||||
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
||||
val pad = ProfileAvatarData(inputStream, outputStream, "image/jpeg", ProfileCipherOutputStreamFactory(profileKey))
|
||||
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, pad.contentType, pad.dataLength, null)
|
||||
val b = Buffer()
|
||||
drb.writeTo(b)
|
||||
val data = b.readByteArray()
|
||||
var id: Long = 0
|
||||
try {
|
||||
id = retryIfNeeded(4) {
|
||||
FileServerAPIV2.upload(data)
|
||||
}.get()
|
||||
} catch (e: Exception) {
|
||||
deferred.reject(e)
|
||||
}
|
||||
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
||||
val url = "${FileServerAPIV2.SERVER}/files/$id"
|
||||
TextSecurePreferences.setProfilePictureURL(context, url)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
}
|
@ -58,7 +58,6 @@ public enum MaterialColor {
|
||||
|
||||
private final String serialized;
|
||||
|
||||
|
||||
MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) {
|
||||
this.mainColor = mainColor;
|
||||
this.tintColor = tintColor;
|
||||
@ -110,9 +109,9 @@ public enum MaterialColor {
|
||||
}
|
||||
|
||||
public boolean represents(Context context, int colorValue) {
|
||||
return context.getResources().getColor(mainColor) == colorValue ||
|
||||
context.getResources().getColor(tintColor) == colorValue ||
|
||||
context.getResources().getColor(shadeColor) == colorValue;
|
||||
return context.getResources().getColor(mainColor) == colorValue
|
||||
|| context.getResources().getColor(tintColor) == colorValue
|
||||
|| context.getResources().getColor(shadeColor) == colorValue;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
|
@ -1,70 +0,0 @@
|
||||
package org.session.libsession.utilities.color;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MaterialColors {
|
||||
|
||||
public static final MaterialColorList CONVERSATION_PALETTE = new MaterialColorList(new ArrayList<>(Arrays.asList(
|
||||
MaterialColor.PLUM,
|
||||
MaterialColor.CRIMSON,
|
||||
MaterialColor.VERMILLION,
|
||||
MaterialColor.VIOLET,
|
||||
MaterialColor.BLUE,
|
||||
MaterialColor.INDIGO,
|
||||
MaterialColor.FOREST,
|
||||
MaterialColor.WINTERGREEN,
|
||||
MaterialColor.TEAL,
|
||||
MaterialColor.BURLAP,
|
||||
MaterialColor.TAUPE,
|
||||
MaterialColor.STEEL
|
||||
)));
|
||||
|
||||
public static class MaterialColorList {
|
||||
|
||||
private final List<MaterialColor> colors;
|
||||
|
||||
private MaterialColorList(List<MaterialColor> colors) {
|
||||
this.colors = colors;
|
||||
}
|
||||
|
||||
public MaterialColor get(int index) {
|
||||
return colors.get(index);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return colors.size();
|
||||
}
|
||||
|
||||
public @Nullable MaterialColor getByColor(Context context, int colorValue) {
|
||||
for (MaterialColor color : colors) {
|
||||
if (color.represents(context, colorValue)) {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int[] asConversationColorArray(@NonNull Context context) {
|
||||
int[] results = new int[colors.size()];
|
||||
int index = 0;
|
||||
|
||||
for (MaterialColor color : colors) {
|
||||
results[index++] = color.toConversationColor(context);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
package org.session.libsession.utilities.color.spans;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan {
|
||||
|
||||
private final float relativeSize;
|
||||
|
||||
public CenterAlignedRelativeSizeSpan(float relativeSize) {
|
||||
|
@ -31,8 +31,6 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -51,13 +49,10 @@ public class SignalServiceCipher {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = SignalServiceCipher.class.getSimpleName();
|
||||
|
||||
private final SessionProtocol sessionProtocolImpl;
|
||||
private final LokiAPIDatabaseProtocol apiDB;
|
||||
|
||||
public SignalServiceCipher(SessionProtocol sessionProtocolImpl,
|
||||
LokiAPIDatabaseProtocol apiDB)
|
||||
public SignalServiceCipher(LokiAPIDatabaseProtocol apiDB)
|
||||
{
|
||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
||||
this.apiDB = apiDB;
|
||||
}
|
||||
|
||||
@ -125,27 +120,7 @@ public class SignalServiceCipher {
|
||||
|
||||
protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException
|
||||
{
|
||||
byte[] paddedMessage;
|
||||
Metadata metadata;
|
||||
|
||||
if (envelope.isClosedGroupCiphertext()) {
|
||||
String groupPublicKey = envelope.getSource();
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
} else if (envelope.isUnidentifiedSender()) {
|
||||
ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair();
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
} else {
|
||||
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||
}
|
||||
byte[] data = PushTransportDetails.getStrippedPaddingMessageBody(paddedMessage);
|
||||
|
||||
return new Plaintext(metadata, data);
|
||||
throw new IllegalStateException("This shouldn't be used anymore");
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.session.libsignal.service.internal.push.http;
|
||||
|
||||
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
|
||||
import org.session.libsignal.service.api.crypto.DigestingOutputStream;
|
||||
|
||||
@ -19,5 +18,4 @@ public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory
|
||||
public DigestingOutputStream createFor(OutputStream wrap) throws IOException {
|
||||
return new AttachmentCipherOutputStream(key, wrap);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
package org.session.libsignal.service.loki.api.crypto
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
|
||||
|
||||
interface SessionProtocol {
|
||||
|
||||
sealed class Exception(val description: String) : kotlin.Exception(description) {
|
||||
// Encryption
|
||||
object NoUserED25519KeyPair : Exception("Couldn't find user ED25519 key pair.")
|
||||
object SigningFailed : Exception("Couldn't sign message.")
|
||||
object EncryptionFailed : Exception("Couldn't encrypt message.")
|
||||
// Decryption
|
||||
object NoData : Exception("Received an empty envelope.")
|
||||
object InvalidGroupPublicKey : Exception("Invalid group public key.")
|
||||
object NoGroupKeyPair : Exception("Missing group key pair.")
|
||||
object DecryptionFailed : Exception("Couldn't decrypt message.")
|
||||
object InvalidSignature : Exception("Invalid message signature.")
|
||||
}
|
||||
/**
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
* @param ciphertext the data to decrypt.
|
||||
* @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
|
||||
*
|
||||
* @return the padded plaintext.
|
||||
*/
|
||||
fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String>
|
||||
}
|
||||
|
||||
object SessionProtocolUtilities {
|
||||
|
||||
fun decryptClosedGroupCiphertext(ciphertext: ByteArray, groupPublicKey: String, apiDB: LokiAPIDatabaseProtocol, sessionProtocolImpl: SessionProtocol): Pair<ByteArray, String> {
|
||||
val encryptionKeyPairs = apiDB.getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList()
|
||||
if (encryptionKeyPairs.isEmpty()) { throw SessionProtocol.Exception.NoGroupKeyPair }
|
||||
// Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
|
||||
// likely be the one we want) but try older ones in case that didn't work)
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
|
||||
fun decrypt(): Pair<ByteArray, String> {
|
||||
try {
|
||||
return sessionProtocolImpl.decrypt(ciphertext, encryptionKeyPair)
|
||||
} catch(exception: Exception) {
|
||||
if (encryptionKeyPairs.isNotEmpty()) {
|
||||
encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
|
||||
return decrypt()
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
return decrypt()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user