mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15: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'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 162
|
def canonicalVersionCode = 163
|
||||||
def canonicalVersionName = "1.10.3"
|
def canonicalVersionName = "1.10.4"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -254,6 +254,10 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.TextSecure.DayNight"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
|
||||||
android:screenOrientation="portrait"
|
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.messaging.threads.Address;
|
||||||
import org.session.libsession.snode.SnodeModule;
|
import org.session.libsession.snode.SnodeModule;
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||||
|
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
import org.session.libsession.utilities.SSKEnvironment;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.Util;
|
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.libsession.utilities.preferences.ProfileKeyUtil;
|
||||||
import org.session.libsignal.service.api.util.StreamDetails;
|
import org.session.libsignal.service.api.util.StreamDetails;
|
||||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
|
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
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.BackgroundPollWorker;
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
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.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
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.WebRtcAudioManager;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -101,6 +104,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
import dagger.ObjectGraph;
|
import dagger.ObjectGraph;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function1;
|
||||||
import kotlinx.coroutines.Job;
|
import kotlinx.coroutines.Job;
|
||||||
import network.loki.messenger.BuildConfig;
|
import network.loki.messenger.BuildConfig;
|
||||||
|
|
||||||
@ -173,8 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
MessagingModuleConfiguration.Companion.configure(this,
|
MessagingModuleConfiguration.Companion.configure(this,
|
||||||
DatabaseFactory.getStorage(this),
|
DatabaseFactory.getStorage(this),
|
||||||
DatabaseFactory.getAttachmentProvider(this),
|
DatabaseFactory.getAttachmentProvider(this));
|
||||||
new SessionProtocolImpl(this));
|
|
||||||
SnodeModule.Companion.configure(apiDB, broadcaster);
|
SnodeModule.Companion.configure(apiDB, broadcaster);
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
|
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
|
||||||
@ -481,21 +484,31 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void resubmitProfilePictureIfNeeded() {
|
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);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||||
if (userPublicKey == null) return;
|
if (userPublicKey == null) return;
|
||||||
long now = new Date().getTime();
|
long now = new Date().getTime();
|
||||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
||||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||||
AsyncTask.execute(() -> {
|
ThreadUtils.queue(() -> {
|
||||||
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this);
|
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
||||||
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey);
|
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
||||||
try {
|
try {
|
||||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
// Read the file into a byte array
|
||||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
|
||||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
int count;
|
||||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
byte[] buffer = new byte[1024];
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
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;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
@ -108,28 +108,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return null // TODO: Implement
|
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 database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||||
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
||||||
attachmentStream.contentType,
|
attachmentStream.contentType,
|
||||||
attachmentKey,
|
attachmentKey,
|
||||||
Optional.of(Util.toIntExact(attachmentStream.length)),
|
Optional.of(Util.toIntExact(attachmentStream.length)),
|
||||||
attachmentStream.preview,
|
attachmentStream.preview,
|
||||||
attachmentStream.width, attachmentStream.height,
|
attachmentStream.width, attachmentStream.height,
|
||||||
Optional.fromNullable(uploadResult.digest),
|
Optional.fromNullable(uploadResult.digest),
|
||||||
attachmentStream.fileName,
|
attachmentStream.fileName,
|
||||||
attachmentStream.voiceNote,
|
attachmentStream.voiceNote,
|
||||||
attachmentStream.caption,
|
attachmentStream.caption,
|
||||||
uploadResult.url);
|
uploadResult.url);
|
||||||
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
|
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
|
||||||
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
|
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateAttachmentAfterUploadFailed(attachmentId: Long) {
|
override fun handleFailedAttachmentUpload(attachmentId: Long) {
|
||||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||||
database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId)
|
database.handleFailedAttachmentUpload(databaseAttachment.attachmentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageID(serverID: Long): Long? {
|
override fun getMessageID(serverID: Long): Long? {
|
||||||
@ -230,23 +230,24 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac
|
|||||||
|
|
||||||
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
|
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
|
||||||
if (TextUtils.isEmpty(location)) { return null }
|
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 {
|
return try {
|
||||||
val id: Long = location!!.toLong()
|
val id = location!!.toLong()
|
||||||
val key: ByteArray = Base64.decode(key!!)
|
val key = Base64.decode(key!!)
|
||||||
SignalServiceAttachmentPointer(id,
|
SignalServiceAttachmentPointer(
|
||||||
contentType,
|
id,
|
||||||
key,
|
contentType,
|
||||||
Optional.of(Util.toIntExact(size)),
|
key,
|
||||||
Optional.absent(),
|
Optional.of(Util.toIntExact(size)),
|
||||||
width,
|
Optional.absent(),
|
||||||
height,
|
width,
|
||||||
Optional.fromNullable(digest),
|
height,
|
||||||
Optional.fromNullable(fileName),
|
Optional.fromNullable(digest),
|
||||||
isVoiceNote,
|
Optional.fromNullable(fileName),
|
||||||
Optional.fromNullable(caption),
|
isVoiceNote,
|
||||||
url)
|
Optional.fromNullable(caption),
|
||||||
|
url
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -384,6 +384,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
||||||
} else if (openGroupV2 != null) {
|
} else if (openGroupV2 != null) {
|
||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
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);
|
View rootView = findViewById(R.id.rootView);
|
||||||
|
@ -546,7 +546,7 @@ public class ConversationFragment extends Fragment
|
|||||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||||
.success(l -> {
|
.success(l -> {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
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 (l.contains(serverID)) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
||||||
@ -569,7 +569,7 @@ public class ConversationFragment extends Fragment
|
|||||||
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
.success(l -> {
|
.success(l -> {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
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)) {
|
if (serverID != null && serverID.equals(serverId)) {
|
||||||
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
|
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
|
||||||
break;
|
break;
|
||||||
|
@ -393,7 +393,7 @@ public class AttachmentDatabase extends Database {
|
|||||||
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
|
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();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
|
||||||
|
@ -351,7 +351,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server)
|
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)
|
return getReceivedMessageTimestamps().contains(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.text.TextUtils;
|
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 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_PROFILE_AVATAR = "profile_avatar";
|
||||||
private static final String KEY_ADDRESS = "address";
|
private static final String KEY_ADDRESS = "address";
|
||||||
@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
|||||||
|
|
||||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
||||||
this(new Job.Parameters.Builder()
|
this(new Job.Parameters.Builder()
|
||||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||||
.setMaxAttempts(3)
|
.setMaxAttempts(10)
|
||||||
.build(),
|
.build(),
|
||||||
recipient,
|
recipient,
|
||||||
profileAvatar);
|
profileAvatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
|
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
|
||||||
super(parameters);
|
super(parameters);
|
||||||
|
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.profileAvatar = profileAvatar;
|
this.profileAvatar = profileAvatar;
|
||||||
}
|
}
|
||||||
@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
|||||||
@Override
|
@Override
|
||||||
public @NonNull
|
public @NonNull
|
||||||
Data serialize() {
|
Data serialize() {
|
||||||
return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar)
|
return new Data.Builder()
|
||||||
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
|
.putString(KEY_PROFILE_AVATAR, profileAvatar)
|
||||||
.build();
|
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -24,7 +24,6 @@ import kotlinx.android.synthetic.main.activity_home.*
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
@ -315,23 +314,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val threadID = thread.threadId
|
val threadID = thread.threadId
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
val threadDB = DatabaseFactory.getThreadDatabase(this)
|
val threadDB = DatabaseFactory.getThreadDatabase(this)
|
||||||
val dialogMessage: String
|
val message: String
|
||||||
if (recipient.isGroupRecipient) {
|
if (recipient.isGroupRecipient) {
|
||||||
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
|
val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull()
|
||||||
if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) {
|
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 {
|
} else {
|
||||||
dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
message = resources.getString(R.string.activity_home_leave_group_dialog_message)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
val dialog = AlertDialog.Builder(this)
|
||||||
dialog.setMessage(dialogMessage)
|
dialog.setMessage(message)
|
||||||
dialog.setPositiveButton(R.string.yes) { _, _ ->
|
dialog.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val context = this@HomeActivity as Context
|
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
|
// Send a leave group message if this is an active closed group
|
||||||
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||||
var isClosedGroup: Boolean
|
var isClosedGroup: Boolean
|
||||||
@ -350,34 +350,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Delete the conversation
|
||||||
withContext(Dispatchers.IO) {
|
val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
if (v1OpenGroup != null) {
|
||||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
if (publicChat != null) {
|
apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
||||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server)
|
||||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server)
|
||||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
|
.removeChat(v1OpenGroup.server, v1OpenGroup.channel)
|
||||||
OpenGroupAPI.leave(publicChat.channel, publicChat.server)
|
} else if (v2OpenGroup != null) {
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||||
.removeChat(publicChat.server, publicChat.channel)
|
apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||||
} else if (openGroupV2 != null) {
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
.removeChat(v2OpenGroup.server, v2OpenGroup.room)
|
||||||
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server)
|
} else {
|
||||||
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server)
|
threadDB.deleteConversation(threadID)
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
|
||||||
.removeChat(openGroupV2.server, openGroupV2.room)
|
|
||||||
} else {
|
|
||||||
threadDB.deleteConversation(threadID)
|
|
||||||
}
|
|
||||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
|
||||||
}
|
}
|
||||||
|
// Update the badge count
|
||||||
|
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
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()
|
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||||
|
@ -10,8 +10,8 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.GridLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.*
|
import androidx.fragment.app.*
|
||||||
@ -42,9 +42,6 @@ import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
|
|||||||
import org.thoughtcrime.securesms.loki.viewmodel.State
|
import org.thoughtcrime.securesms.loki.viewmodel.State
|
||||||
|
|
||||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
|
||||||
private val viewModel by viewModels<DefaultGroupsViewModel>()
|
|
||||||
|
|
||||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
@ -83,23 +80,18 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun joinPublicChatIfPossible(url: String) {
|
fun joinPublicChatIfPossible(url: String) {
|
||||||
// add http if just an IP style / host style URL is entered but leave it if scheme is included
|
// Add "http" if not entered explicitly
|
||||||
val properString = if (!url.startsWith("http")) "http://$url" else url
|
val stringWithExplicitScheme = 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 url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||||
|
val room = url.pathSegments().firstOrNull()
|
||||||
val room = httpUrl.pathSegments().firstOrNull()
|
val publicKey = url.queryParameter("public_key")
|
||||||
val publicKey = httpUrl.queryParameter("public_key")
|
|
||||||
val isV2OpenGroup = !room.isNullOrEmpty()
|
val isV2OpenGroup = !room.isNullOrEmpty()
|
||||||
showLoader()
|
showLoader()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val (threadID, groupID) = if (isV2OpenGroup) {
|
val (threadID, groupID) = if (isV2OpenGroup) {
|
||||||
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply {
|
||||||
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server
|
||||||
// non-standard port, add to server
|
|
||||||
this.port(httpUrl.port())
|
|
||||||
}
|
|
||||||
}.build()
|
}.build()
|
||||||
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
|
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
@ -107,21 +99,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
threadID to groupID
|
threadID to groupID
|
||||||
} else {
|
} else {
|
||||||
val channel: Long = 1
|
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 threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||||
threadID to groupID
|
threadID to groupID
|
||||||
}
|
}
|
||||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// go to the new conversation and finish this one
|
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
|
||||||
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false))
|
openConversationActivity(this@JoinPublicChatActivity, threadID, recipient)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("JoinPublicChatActivity", "Failed to join open group.", e)
|
Log.e("Loki", "Couldn't join open group.", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
hideLoader()
|
hideLoader()
|
||||||
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
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
|
// region Enter Chat URL Fragment
|
||||||
class EnterChatURLFragment : Fragment() {
|
class EnterChatURLFragment : Fragment() {
|
||||||
|
|
||||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
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>) {
|
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||||
defaultRoomsGridLayout.removeAllViews()
|
defaultRoomsGridLayout.removeAllViews()
|
||||||
|
defaultRoomsGridLayout.useDefaultMargins = false
|
||||||
groups.forEach { defaultGroup ->
|
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 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 {
|
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||||
isCircular = true
|
isCircular = true
|
||||||
}
|
}
|
||||||
@ -197,35 +208,14 @@ class EnterChatURLFragment : Fragment() {
|
|||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultRoomsGridLayout.addView(chip)
|
defaultRoomsGridLayout.addView(chip)
|
||||||
}
|
}
|
||||||
if (groups.size and 1 != 0) {
|
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||||
// add a filler weight 1 view
|
|
||||||
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
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
|
// region Convenience
|
||||||
private fun joinPublicChatIfPossible() {
|
private fun joinPublicChatIfPossible() {
|
||||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
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 network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.all
|
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.alwaysUi
|
||||||
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
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.open_groups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.threads.Address
|
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.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.session.libsignal.service.api.util.StreamDetails
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
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.profiles.ProfileMediaConstraints
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -127,7 +123,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
||||||
if (resultCode != Activity.RESULT_OK) { return }
|
if (resultCode != Activity.RESULT_OK) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
||||||
var inputFile: Uri? = data?.data
|
var inputFile: Uri? = data?.data
|
||||||
if (inputFile == null && tempFile != null) {
|
if (inputFile == null && tempFile != null) {
|
||||||
@ -136,7 +134,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
||||||
}
|
}
|
||||||
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
||||||
if (resultCode != Activity.RESULT_OK) { return }
|
if (resultCode != Activity.RESULT_OK) {
|
||||||
|
return
|
||||||
|
}
|
||||||
AsyncTask.execute {
|
AsyncTask.execute {
|
||||||
try {
|
try {
|
||||||
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
||||||
@ -186,42 +186,28 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
val profilePicture = profilePictureToBeUploaded
|
val profilePicture = profilePictureToBeUploaded
|
||||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||||
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
|
||||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||||
val storageAPI = FileServerAPI.shared
|
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
val compoundPromise = all(promises)
|
||||||
all(promises).bind {
|
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
|
||||||
// updating the profile name or picture
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||||
if (profilePicture != null || displayName != null) {
|
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||||
task {
|
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
|
||||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
|
||||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
|
||||||
}
|
|
||||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Promise.of(Unit)
|
|
||||||
}
|
}
|
||||||
}.alwaysUi {
|
if (profilePicture != null || displayName != null) {
|
||||||
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compoundPromise.alwaysUi {
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
btnGroupNameDisplay.text = displayName
|
btnGroupNameDisplay.text = displayName
|
||||||
}
|
}
|
||||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||||
profilePictureView.recycle() // clear cached image before update tje profilePictureView
|
profilePictureView.recycle() // Clear the cached image before updating
|
||||||
profilePictureView.update()
|
profilePictureView.update()
|
||||||
}
|
}
|
||||||
displayNameToBeUploaded = null
|
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.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
@ -138,9 +139,10 @@ class PublicChatManager(private val context: Context) {
|
|||||||
val groupId = OpenGroup.getId(channel, server)
|
val groupId = OpenGroup.getId(channel, server)
|
||||||
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||||
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
||||||
GroupManager.deleteGroup(groupAddress, context)
|
ThreadUtils.queue {
|
||||||
|
GroupManager.deleteGroup(groupAddress, context) // Must be invoked on a background thread
|
||||||
Util.runOnMain { startPollersIfNeeded() }
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeChat(server: String, room: String) {
|
fun removeChat(server: String, room: String) {
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.database
|
|||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
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) {
|
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(3)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.serverID, serverID)
|
contentValues.put(Companion.serverID, serverID)
|
||||||
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
|
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 {
|
fun getOriginalThreadID(messageID: Long): Long {
|
||||||
@ -114,11 +115,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
|
|
||||||
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
|
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(3)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.serverID, serverID)
|
contentValues.put(Companion.serverID, serverID)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
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? {
|
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 {
|
fun isJobCanceled(job: Job): Boolean {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
var cursor: android.database.Cursor? = null
|
var cursor: android.database.Cursor? = null
|
||||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.protobuf.ByteString
|
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.DjbECPrivateKey
|
||||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
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.database.GroupDatabase
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
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.Address
|
||||||
import org.session.libsession.messaging.threads.GroupRecord
|
import org.session.libsession.messaging.threads.GroupRecord
|
||||||
@ -195,7 +191,7 @@ object ClosedGroupsProtocolV2 {
|
|||||||
}
|
}
|
||||||
if (userPublicKey in admins) {
|
if (userPublicKey in admins) {
|
||||||
// send current encryption key to the latest added members
|
// send current encryption key to the latest added members
|
||||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||||
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||||
if (encryptionKeyPair == null) {
|
if (encryptionKeyPair == null) {
|
||||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
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
|
// Find our wrapper and decrypt it if possible
|
||||||
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
|
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
|
||||||
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
|
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
|
||||||
val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first
|
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||||
// Parse it
|
// Parse it
|
||||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
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 groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
|
||||||
val application = ApplicationContext.getInstance(context)
|
val application = ApplicationContext.getInstance(context)
|
||||||
|
|
||||||
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
|
||||||
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
storage.removeLastDeletionServerId(room, server)
|
storage.removeLastDeletionServerId(room, server)
|
||||||
storage.removeLastMessageServerId(room, server)
|
storage.removeLastMessageServerId(room, server)
|
||||||
|
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,4 @@ class DefaultGroupsViewModel : ViewModel() {
|
|||||||
}.onStart {
|
}.onStart {
|
||||||
emit(State.Loading)
|
emit(State.Loading)
|
||||||
}.asLiveData()
|
}.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:layout_marginTop="4dp"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textColor="@color/text"
|
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
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textColor="@color/text"
|
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>
|
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||||
|
|
||||||
|
@ -33,23 +33,27 @@
|
|||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:paddingHorizontal="24dp"
|
android:id="@+id/defaultRoomsContainer"
|
||||||
android:id="@+id/defaultRoomsParent"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_marginVertical="16dp"
|
android:layout_marginVertical="16dp"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
android:text="@string/activity_join_public_chat_join_rooms"
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<GridLayout
|
<GridLayout
|
||||||
android:id="@+id/defaultRoomsGridLayout"
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
android:columnCount="2"
|
android:columnCount="2"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<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:layout_marginTop="4dp"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textColor="@color/text"
|
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
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textSize="@dimen/very_small_font_size"
|
android:textSize="@dimen/very_small_font_size"
|
||||||
android:textColor="@color/text"
|
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>
|
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||||
|
|
||||||
|
@ -134,6 +134,12 @@
|
|||||||
android:background="?android:dividerHorizontal"
|
android:background="?android:dividerHorizontal"
|
||||||
android:elevation="1dp" />
|
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
|
<FrameLayout
|
||||||
android:id="@+id/fragment_content"
|
android:id="@+id/fragment_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:theme="@style/Theme.MaterialComponents.DayNight"
|
android:theme="@style/Theme.MaterialComponents.DayNight"
|
||||||
style="?attr/chipStyle"
|
style="?attr/chipStyle"
|
||||||
app:chipStartPadding="6dp"
|
app:chipStartPadding="4dp"
|
||||||
|
app:chipBackgroundColor="@color/open_group_chip_color"
|
||||||
android:layout_columnWeight="1"
|
android:layout_columnWeight="1"
|
||||||
android:layout_marginHorizontal="2dp"
|
|
||||||
tools:text="Main Group"
|
tools:text="Main Group"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
tools:layout_width="wrap_content"
|
tools:layout_width="wrap_content"
|
||||||
|
app:chipMinTouchTargetSize="0dp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="52dp" />
|
android:layout_height="wrap_content" />
|
@ -33,23 +33,27 @@
|
|||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:paddingHorizontal="24dp"
|
android:id="@+id/defaultRoomsContainer"
|
||||||
android:id="@+id/defaultRoomsParent"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_marginVertical="16dp"
|
android:layout_marginVertical="16dp"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
android:text="@string/activity_join_public_chat_join_rooms"
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<GridLayout
|
<GridLayout
|
||||||
android:id="@+id/defaultRoomsGridLayout"
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
android:columnCount="2"
|
android:columnCount="2"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<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="new_conversation_button_collapsed_background">#F5F5F5</color>
|
||||||
<color name="pn_option_background">#FCFCFC</color>
|
<color name="pn_option_background">#FCFCFC</color>
|
||||||
<color name="fake_chat_bubble_background">#F5F5F5</color>
|
<color name="fake_chat_bubble_background">#F5F5F5</color>
|
||||||
|
<color name="open_group_chip_color">#0D000000</color>
|
||||||
|
|
||||||
|
|
||||||
<color name="default_background_start">#ffffff</color>
|
<color name="default_background_start">#ffffff</color>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
<color name="pn_option_background">#1B1B1B</color>
|
<color name="pn_option_background">#1B1B1B</color>
|
||||||
<color name="pn_option_border">#212121</color>
|
<color name="pn_option_border">#212121</color>
|
||||||
<color name="paths_building">#FFCE3A</color>
|
<color name="paths_building">#FFCE3A</color>
|
||||||
|
<color name="open_group_chip_color">#0DFFFFFF</color>
|
||||||
|
|
||||||
<array name="profile_picture_placeholder_colors">
|
<array name="profile_picture_placeholder_colors">
|
||||||
<item>#5ff8b0</item>
|
<item>#5ff8b0</item>
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
android:dependency="pref_key_enable_notifications"
|
android:dependency="pref_key_enable_notifications"
|
||||||
android:key="pref_key_use_fcm"
|
android:key="pref_key_use_fcm"
|
||||||
android:title="Use Fast Mode"
|
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" />
|
android:defaultValue="false" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
@ -29,8 +29,8 @@ interface MessageDataProvider {
|
|||||||
|
|
||||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||||
|
|
||||||
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||||
|
|
||||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||||
|
@ -2,13 +2,11 @@ package org.session.libsession.messaging
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
|
||||||
|
|
||||||
class MessagingModuleConfiguration(
|
class MessagingModuleConfiguration(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val storage: StorageProtocol,
|
val storage: StorageProtocol,
|
||||||
val messageDataProvider: MessageDataProvider,
|
val messageDataProvider: MessageDataProvider
|
||||||
val sessionProtocol: SessionProtocol
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -16,11 +14,10 @@ class MessagingModuleConfiguration(
|
|||||||
|
|
||||||
fun configure(context: Context,
|
fun configure(context: Context,
|
||||||
storage: StorageProtocol,
|
storage: StorageProtocol,
|
||||||
messageDataProvider: MessageDataProvider,
|
messageDataProvider: MessageDataProvider
|
||||||
sessionProtocol: SessionProtocol
|
|
||||||
) {
|
) {
|
||||||
if (Companion::shared.isInitialized) { return }
|
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)
|
fun removeLastDeletionServerId(room: String, server: String)
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
fun isDuplicateMessage(timestamp: Long, sender: String): Boolean
|
||||||
fun getReceivedMessageTimestamps(): Set<Long>
|
fun getReceivedMessageTimestamps(): Set<Long>
|
||||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||||
|
@ -15,8 +15,8 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
|
|
||||||
object FileServerAPIV2 {
|
object FileServerAPIV2 {
|
||||||
|
|
||||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
private const val SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
const val SERVER = "http://88.99.175.227"
|
||||||
|
|
||||||
sealed class Error(message: String) : Exception(message) {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object ParsingFailed : Error("Invalid response.")
|
object ParsingFailed : Error("Invalid response.")
|
||||||
@ -43,7 +43,7 @@ object FileServerAPIV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
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()
|
val urlBuilder = HttpUrl.Builder()
|
||||||
.scheme(url.scheme())
|
.scheme(url.scheme())
|
||||||
.host(url.host())
|
.host(url.host())
|
||||||
@ -64,7 +64,7 @@ object FileServerAPIV2 {
|
|||||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
}
|
}
|
||||||
if (request.useOnionRouting) {
|
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)
|
Log.e("Loki", "File server request failed.", e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
||||||
if (exception == Error.NoAttachment) {
|
if (exception == Error.NoAttachment) {
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||||
this.handlePermanentFailure(exception)
|
this.handlePermanentFailure(exception)
|
||||||
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
||||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
||||||
// got a "Cannot GET ..." error from the file server.
|
// 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)
|
this.handlePermanentFailure(exception)
|
||||||
} else {
|
} else {
|
||||||
this.handleFailure(exception)
|
this.handleFailure(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
|
||||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
val tempFile = createTempFile()
|
val tempFile = createTempFile()
|
||||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
|
||||||
val stream = if (openGroupV2 == null) {
|
val inputStream = if (openGroupV2 == null) {
|
||||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, 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
|
// 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()) {
|
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||||
@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val url = HttpUrl.parse(attachment.url)!!
|
val url = HttpUrl.parse(attachment.url)!!
|
||||||
val fileId = url.pathSegments().last()
|
val fileID = url.pathSegments().last()
|
||||||
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||||
tempFile.writeBytes(it)
|
tempFile.writeBytes(it)
|
||||||
}
|
}
|
||||||
FileInputStream(tempFile)
|
FileInputStream(tempFile)
|
||||||
}
|
}
|
||||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -3,8 +3,12 @@ package org.session.libsession.messaging.jobs
|
|||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.Kryo
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okio.Buffer
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
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.messages.Message
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
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.crypto.PaddingInputStream
|
||||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
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.AttachmentCipherOutputStreamFactory
|
||||||
|
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
|
||||||
import org.session.libsignal.service.internal.util.Util
|
import org.session.libsignal.service.internal.util.Util
|
||||||
import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
|
import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
|
||||||
import org.session.libsignal.utilities.logging.Log
|
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 {
|
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||||
override var delegate: JobDelegate? = null
|
override var delegate: JobDelegate? = null
|
||||||
@ -45,27 +51,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
try {
|
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)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
val usePadding = false
|
val v2OpenGroup = storage.getV2OpenGroup(threadID)
|
||||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
val v1OpenGroup = storage.getOpenGroup(threadID)
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
if (v2OpenGroup != null) {
|
||||||
val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server
|
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
||||||
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
||||||
val attachmentKey = Util.getSecretBytes(64)
|
}
|
||||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
} else if (v1OpenGroup == null) {
|
||||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
|
||||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
FileServerAPIV2.upload(it)
|
||||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
}
|
||||||
val uploadResult = if (openGroupV2 != null) {
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
val dataBytes = attachmentData.data.readBytes()
|
} else { // V1 open group
|
||||||
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
val server = v1OpenGroup.server
|
||||||
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
|
||||||
} else {
|
attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
|
||||||
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
val result = FileServerAPI.shared.uploadAttachment(server, pushData)
|
||||||
|
handleSuccess(attachment, ByteArray(0), result)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
if (e == Error.NoAttachment) {
|
if (e == Error.NoAttachment) {
|
||||||
this.handlePermanentFailure(e)
|
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) {
|
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||||
Log.d(TAG, "Attachment uploaded successfully.")
|
Log.d(TAG, "Attachment uploaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this)
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||||
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermanentFailure(e: Exception) {
|
private fun handlePermanentFailure(e: Exception) {
|
||||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||||
delegate?.handleJobFailedPermanently(this, e)
|
delegate?.handleJobFailedPermanently(this, e)
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
|
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
|
||||||
failAssociatedMessageSendJob(e)
|
failAssociatedMessageSendJob(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,12 +130,7 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
// Message send jobs waiting for the attachment to upload
|
// Message send jobs waiting for the attachment to upload
|
||||||
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
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.")
|
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
|
return
|
||||||
}
|
}
|
||||||
// Regular job failure
|
// Regular job failure
|
||||||
|
@ -348,7 +348,7 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
// region General
|
// region General
|
||||||
@Suppress("UNCHECKED_CAST")
|
@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 authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val requests = rooms.mapNotNull { room ->
|
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 android.util.Log
|
||||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
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.hexEncodedPublicKey
|
||||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
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()) }
|
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 recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||||
val signatureSize = Sign.BYTES
|
val signatureSize = Sign.BYTES
|
||||||
@ -32,9 +35,9 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
|||||||
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.d("Loki", "Couldn't decrypt message due to error: $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
|
// 2. ) Get the message parts
|
||||||
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
||||||
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
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)
|
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
||||||
try {
|
try {
|
||||||
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
||||||
if (!isValid) { throw SessionProtocol.Exception.InvalidSignature }
|
if (!isValid) { throw MessageReceiver.Error.InvalidSignature }
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.d("Loki", "Couldn't verify message signature due to error: $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
|
// 4. ) Get the sender's X25519 public key
|
||||||
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
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.Hex
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
object MessageSenderEncryption {
|
object MessageEncrypter {
|
||||||
|
|
||||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ object MessageSenderEncryption {
|
|||||||
*
|
*
|
||||||
* @return the encrypted message.
|
* @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 context = MessagingModuleConfiguration.shared.context
|
||||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
||||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
@ -10,35 +10,24 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
|||||||
|
|
||||||
object MessageReceiver {
|
object MessageReceiver {
|
||||||
|
|
||||||
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
internal sealed class Error(message: String) : Exception(message) {
|
||||||
|
|
||||||
internal sealed class Error(val description: String) : Exception(description) {
|
|
||||||
object DuplicateMessage: Error("Duplicate message.")
|
object DuplicateMessage: Error("Duplicate message.")
|
||||||
object InvalidMessage: Error("Invalid message.")
|
object InvalidMessage: Error("Invalid message.")
|
||||||
object UnknownMessage: Error("Unknown message type.")
|
object UnknownMessage: Error("Unknown message type.")
|
||||||
object UnknownEnvelopeType: Error("Unknown envelope type.")
|
object UnknownEnvelopeType: Error("Unknown envelope type.")
|
||||||
object NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.")
|
object DecryptionFailed : Exception("Couldn't decrypt message.")
|
||||||
object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.")
|
|
||||||
object InvalidSignature: Error("Invalid message signature.")
|
object InvalidSignature: Error("Invalid message signature.")
|
||||||
object NoData: Error("Received an empty envelope.")
|
object NoData: Error("Received an empty envelope.")
|
||||||
object SenderBlocked: Error("Received a message from a blocked user.")
|
object SenderBlocked: Error("Received a message from a blocked user.")
|
||||||
object NoThread: Error("Couldn't find thread for message.")
|
object NoThread: Error("Couldn't find thread for message.")
|
||||||
object SelfSend: Error("Message addressed at self.")
|
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 InvalidGroupPublicKey: Error("Invalid group public key.")
|
||||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||||
|
|
||||||
internal val isRetryable: Boolean = when (this) {
|
internal val isRetryable: Boolean = when (this) {
|
||||||
is DuplicateMessage -> false
|
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||||
is InvalidMessage -> false
|
is UnknownEnvelopeType, is InvalidSignature, is NoData,
|
||||||
is UnknownMessage -> false
|
is SenderBlocked, is SelfSend -> false
|
||||||
is UnknownEnvelopeType -> false
|
|
||||||
is InvalidSignature -> false
|
|
||||||
is NoData -> false
|
|
||||||
is NoThread -> false
|
|
||||||
is SenderBlocked -> false
|
|
||||||
is SelfSend -> false
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,13 +35,15 @@ object MessageReceiver {
|
|||||||
internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> {
|
internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
val isOpenGroupMessage = openGroupServerID != null
|
val isOpenGroupMessage = (openGroupServerID != null)
|
||||||
// Parse the envelope
|
// Parse the envelope
|
||||||
val envelope = SignalServiceProtos.Envelope.parseFrom(data)
|
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
|
// 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
|
// 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.
|
// 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
|
// Decrypt the contents
|
||||||
val ciphertext = envelope.content ?: throw Error.NoData
|
val ciphertext = envelope.content ?: throw Error.NoData
|
||||||
var plaintext: ByteArray? = null
|
var plaintext: ByteArray? = null
|
||||||
@ -65,7 +56,7 @@ object MessageReceiver {
|
|||||||
when (envelope.type) {
|
when (envelope.type) {
|
||||||
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
||||||
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
||||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair)
|
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
|
||||||
plaintext = decryptionResult.first
|
plaintext = decryptionResult.first
|
||||||
sender = decryptionResult.second
|
sender = decryptionResult.second
|
||||||
}
|
}
|
||||||
@ -81,7 +72,7 @@ object MessageReceiver {
|
|||||||
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||||
fun decrypt() {
|
fun decrypt() {
|
||||||
try {
|
try {
|
||||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), encryptionKeyPair)
|
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), encryptionKeyPair)
|
||||||
plaintext = decryptionResult.first
|
plaintext = decryptionResult.first
|
||||||
sender = decryptionResult.second
|
sender = decryptionResult.second
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -100,9 +91,9 @@ object MessageReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Don't process the envelope any further if the message has been handled already
|
// 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
|
// 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
|
// Parse the proto
|
||||||
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
|
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
|
||||||
// Parse the message
|
// Parse the message
|
||||||
@ -113,7 +104,7 @@ object MessageReceiver {
|
|||||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||||
ConfigurationMessage.fromProto(proto) ?:
|
ConfigurationMessage.fromProto(proto) ?:
|
||||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||||
// Ignore self sends if needed
|
// Ignore self send if needed
|
||||||
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
||||||
// Guard against control messages in open groups
|
// Guard against control messages in open groups
|
||||||
if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage
|
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.messages.visible.*
|
||||||
import org.session.libsession.messaging.open_groups.*
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.threads.Address
|
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.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.snode.RawResponsePromise
|
import org.session.libsession.snode.RawResponsePromise
|
||||||
import org.session.libsession.snode.SnodeAPI
|
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.service.loki.utilities.hexEncodedPublicKey
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.logging.Log
|
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.attachments.Attachment as SignalAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
|
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) {
|
sealed class Error(val description: String) : Exception(description) {
|
||||||
object InvalidMessage : Error("Invalid message.")
|
object InvalidMessage : Error("Invalid message.")
|
||||||
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
|
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 NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||||
object SigningFailed : Error("Couldn't sign message.")
|
object SigningFailed : Error("Couldn't sign message.")
|
||||||
object EncryptionFailed : Error("Couldn't encrypt message.")
|
object EncryptionFailed : Error("Couldn't encrypt message.")
|
||||||
@ -46,17 +44,10 @@ object MessageSender {
|
|||||||
// Closed groups
|
// Closed groups
|
||||||
object NoThread : Error("Couldn't find a thread associated with the given group public key.")
|
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 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.")
|
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
||||||
|
|
||||||
// Precondition
|
|
||||||
class PreconditionFailure(val reason: String): Error(reason)
|
|
||||||
|
|
||||||
internal val isRetryable: Boolean = when (this) {
|
internal val isRetryable: Boolean = when (this) {
|
||||||
is InvalidMessage -> false
|
is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false
|
||||||
is ProtoConversionFailed -> false
|
|
||||||
is ProofOfWorkCalculationFailed -> false
|
|
||||||
is InvalidClosedGroupUpdate -> false
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +67,9 @@ object MessageSender {
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
// Set the timestamp, sender and recipient
|
// 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
|
message.sender = userPublicKey
|
||||||
val isSelfSend = (message.recipient == userPublicKey)
|
val isSelfSend = (message.recipient == userPublicKey)
|
||||||
// Set the failure handler (need it here already for precondition failure handling)
|
// Set the failure handler (need it here already for precondition failure handling)
|
||||||
@ -91,8 +84,7 @@ object MessageSender {
|
|||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> message.recipient = destination.publicKey
|
is Destination.Contact -> message.recipient = destination.publicKey
|
||||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||||
is Destination.OpenGroup,
|
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -125,13 +117,12 @@ object MessageSender {
|
|||||||
// Encrypt the serialized protobuf
|
// Encrypt the serialized protobuf
|
||||||
val ciphertext: ByteArray
|
val ciphertext: ByteArray
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey)
|
is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||||
is Destination.ClosedGroup -> {
|
is Destination.ClosedGroup -> {
|
||||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
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.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -145,8 +136,7 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup,
|
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -201,7 +191,9 @@ object MessageSender {
|
|||||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
if (message.sentTimestamp == null) {
|
||||||
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
|
}
|
||||||
message.sender = storage.getUserPublicKey()
|
message.sender = storage.getUserPublicKey()
|
||||||
// Set the failure handler (need it here already for precondition failure handling)
|
// Set the failure handler (need it here already for precondition failure handling)
|
||||||
fun handleFailure(error: Exception) {
|
fun handleFailure(error: Exception) {
|
||||||
@ -210,18 +202,15 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
||||||
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
|
||||||
is Destination.OpenGroup -> {
|
is Destination.OpenGroup -> {
|
||||||
message.recipient = "${destination.server}.${destination.channel}"
|
message.recipient = "${destination.server}.${destination.channel}"
|
||||||
val server = destination.server
|
val server = destination.server
|
||||||
val channel = destination.channel
|
val channel = destination.channel
|
||||||
|
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the message to an open group message
|
// Convert the message to an open group message
|
||||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
@ -239,7 +228,6 @@ object MessageSender {
|
|||||||
message.recipient = "${destination.server}.${destination.room}"
|
message.recipient = "${destination.server}.${destination.room}"
|
||||||
val server = destination.server
|
val server = destination.server
|
||||||
val room = destination.room
|
val room = destination.room
|
||||||
|
|
||||||
// Attach the user's profile if needed
|
// Attach the user's profile if needed
|
||||||
if (message is VisibleMessage) {
|
if (message is VisibleMessage) {
|
||||||
val displayName = storage.getUserDisplayName()!!
|
val displayName = storage.getUserDisplayName()!!
|
||||||
@ -251,20 +239,17 @@ object MessageSender {
|
|||||||
message.profile = Profile(displayName)
|
message.profile = Profile(displayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
val proto = message.toProto()!!
|
val proto = message.toProto()!!
|
||||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||||
val openGroupMessage = OpenGroupMessageV2(
|
val openGroupMessage = OpenGroupMessageV2(
|
||||||
sender = message.sender,
|
sender = message.sender,
|
||||||
sentTimestamp = message.sentTimestamp!!,
|
sentTimestamp = message.sentTimestamp!!,
|
||||||
base64EncodedData = Base64.encodeBytes(plaintext),
|
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||||
)
|
)
|
||||||
|
|
||||||
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||||
message.openGroupServerMessageID = it.serverID
|
message.openGroupServerMessageID = it.serverID
|
||||||
handleSuccessfulMessageSend(message, destination)
|
handleSuccessfulMessageSend(message, destination)
|
||||||
@ -272,7 +257,6 @@ object MessageSender {
|
|||||||
}.fail {
|
}.fail {
|
||||||
handleFailure(it)
|
handleFailure(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
@ -285,7 +269,7 @@ object MessageSender {
|
|||||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
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
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
@ -293,7 +277,7 @@ object MessageSender {
|
|||||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||||
if (threadID != null && threadID >= 0) {
|
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
|
// Mark the message as sent
|
||||||
@ -323,16 +307,16 @@ object MessageSender {
|
|||||||
// Convenience
|
// Convenience
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
|
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
|
||||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!)
|
val attachmentIDs = messageDataProvider.getAttachmentIDsFor(message.id!!)
|
||||||
message.attachmentIDs.addAll(attachmentIDs)
|
message.attachmentIDs.addAll(attachmentIDs)
|
||||||
message.quote = Quote.from(quote)
|
message.quote = Quote.from(quote)
|
||||||
message.linkPreview = LinkPreview.from(linkPreview)
|
message.linkPreview = LinkPreview.from(linkPreview)
|
||||||
message.linkPreview?.let {
|
message.linkPreview?.let { linkPreview ->
|
||||||
if (it.attachmentID == null) {
|
if (linkPreview.attachmentID == null) {
|
||||||
dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let {
|
messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
|
||||||
message.linkPreview!!.attachmentID = it
|
message.linkPreview!!.attachmentID = attachmentID
|
||||||
message.attachmentIDs.remove(it)
|
message.attachmentIDs.remove(attachmentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,8 @@ import java.util.*
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
const val groupSizeLimit = 100
|
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> {
|
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||||
val deferred = deferred<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 admins = setOf( userPublicKey )
|
||||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(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)
|
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||||
// Send a closed group update message to all members individually
|
// Send a closed group update message to all members individually
|
||||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
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.")
|
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||||
throw Error.InvalidClosedGroupUpdate
|
throw Error.InvalidClosedGroupUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the new group members
|
// Save the new group members
|
||||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||||
// Update the zombie list
|
// Update the zombie list
|
||||||
val oldZombies = storage.getZombieMember(groupID)
|
val oldZombies = storage.getZombieMember(groupID)
|
||||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||||
|
|
||||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Send the update to the group
|
// Send the update to the group
|
||||||
@ -194,17 +193,14 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
|||||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||||
closedGroupControlMessage.sentTimestamp = sentTime
|
closedGroupControlMessage.sentTimestamp = sentTime
|
||||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||||
|
// Send the new encryption key pair to the remaining group members.
|
||||||
// Send the new encryption key pair to the remaining group members
|
// At this stage we know the user is admin, no need to test.
|
||||||
// At this stage we know the user is admin, no need to test
|
|
||||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
|
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||||
// Insert an outgoing notification
|
|
||||||
// we don't display zombie members in the notification as users have already been notified when those members left
|
|
||||||
val notificationMembers = membersToRemove.minus(oldZombies)
|
val notificationMembers = membersToRemove.minus(oldZombies)
|
||||||
if (notificationMembers.isNotEmpty()) {
|
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 infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime)
|
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
|
// Generate the new encryption key pair
|
||||||
val newKeyPair = Curve.generateKeyPair()
|
val newKeyPair = Curve.generateKeyPair()
|
||||||
// replace call will not succeed if no value already set
|
// Replace call will not succeed if no value already set
|
||||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
pendingKeyPairs.putIfAbsent(groupPublicKey,Optional.absent())
|
||||||
do {
|
do {
|
||||||
// make sure we set the pendingKeyPair or wait until it is not null
|
// Make sure we set the pending key pair or wait until it is not null
|
||||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
} while (!pendingKeyPairs.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||||
// Distribute it
|
// Distribute it
|
||||||
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
||||||
// Store it * after * having sent out the message to the group
|
// Store it * after * having sent out the message to the group
|
||||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
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())
|
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||||
val plaintext = proto.build().toByteArray()
|
val plaintext = proto.build().toByteArray()
|
||||||
val wrappers = targetMembers.map { publicKey ->
|
val wrappers = targetMembers.map { publicKey ->
|
||||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||||
}
|
}
|
||||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||||
@ -307,14 +303,14 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Get the latest encryption key pair
|
// Get the latest encryption key pair
|
||||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||||
// Send it
|
// Send it
|
||||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||||
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||||
val plaintext = proto.build().toByteArray()
|
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.")
|
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||||
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||||
|
@ -35,7 +35,7 @@ import java.security.MessageDigest
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
internal fun MessageReceiver.isBlock(publicKey: String): Boolean {
|
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||||
return recipient.isBlocked
|
return recipient.isBlocked
|
||||||
@ -96,12 +96,9 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data Extraction Notification handling
|
|
||||||
|
|
||||||
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
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
|
if (message.groupPublicKey != null) return
|
||||||
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val senderPublicKey = message.sender!!
|
val senderPublicKey = message.sender!!
|
||||||
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
||||||
@ -112,20 +109,20 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
|
|||||||
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
|
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration message handling
|
|
||||||
|
|
||||||
private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
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()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
|
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
|
||||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||||
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
||||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
for (closeGroup in message.closedGroups) {
|
for (closedGroup in message.closedGroups) {
|
||||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
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 allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||||
@ -137,14 +134,12 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||||
storage.setDisplayName(userPublicKey, 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)
|
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||||
// handle profile photo
|
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
|
||||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
storage.addContacts(message.contacts)
|
storage.addContacts(message.contacts)
|
||||||
}
|
}
|
||||||
@ -159,41 +154,32 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
|
|
||||||
// Get or create thread
|
// 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
|
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||||
|
|
||||||
if (threadID < 0) {
|
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
|
throw MessageReceiver.Error.NoThread
|
||||||
}
|
}
|
||||||
|
|
||||||
val openGroup = threadID.let {
|
val openGroup = threadID.let {
|
||||||
storage.getOpenGroup(it.toString())
|
storage.getOpenGroup(it.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile if needed
|
// Update profile if needed
|
||||||
val newProfile = message.profile
|
val profile = message.profile
|
||||||
|
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
||||||
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
|
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||||
val displayName = newProfile.displayName!!
|
val displayName = profile.displayName!!
|
||||||
if (displayName.isNotEmpty()) {
|
if (displayName.isNotEmpty()) {
|
||||||
profileManager.setDisplayName(context, recipient, displayName)
|
profileManager.setDisplayName(context, recipient, displayName)
|
||||||
}
|
}
|
||||||
if (newProfile.profileKey?.isNotEmpty() == true
|
if (profile.profileKey?.isNotEmpty() == true && profile.profilePictureURL?.isNotEmpty() == true
|
||||||
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) {
|
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profile.profileKey))) {
|
||||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
profileManager.setProfileKey(context, recipient, profile.profileKey!!)
|
||||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||||
val newUrl = newProfile.profilePictureURL
|
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
|
||||||
if (!newUrl.isNullOrEmpty()) {
|
|
||||||
profileManager.setProfilePictureURL(context, recipient, newUrl)
|
|
||||||
if (userPublicKey == message.sender) {
|
|
||||||
profileManager.updateOpenGroupProfilePicturesIfNeeded(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parse quote if needed
|
// Parse quote if needed
|
||||||
@ -201,10 +187,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||||
val quote = proto.dataMessage.quote
|
val quote = proto.dataMessage.quote
|
||||||
val author = Address.fromSerialized(quote.author)
|
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) {
|
if (messageInfo != null) {
|
||||||
val attachments = if (messageInfo.second) MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||||
quoteModel = QuoteModel(quote.id, author, MessagingModuleConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
quoteModel = QuoteModel(quote.id, author, messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||||
} else {
|
} else {
|
||||||
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
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 attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
|
||||||
val attachment = Attachment.fromProto(proto)
|
val attachment = Attachment.fromProto(proto)
|
||||||
if (!attachment.isValid()) {
|
if (!attachment.isValid()) {
|
||||||
@ -233,7 +221,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
return@mapNotNull attachment
|
return@mapNotNull attachment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parse stickers if needed
|
|
||||||
// Persist the message
|
// Persist the message
|
||||||
message.threadID = threadID
|
message.threadID = threadID
|
||||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
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
|
val openGroupServerID = message.openGroupServerMessageID
|
||||||
if (openGroupServerID != null) {
|
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
|
// Cancel any typing indicators if needed
|
||||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
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!!)
|
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) {
|
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 context = MessagingModuleConfiguration.shared.context
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
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) })
|
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||||
} else {
|
} else {
|
||||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
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)
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (userPublicKey == sender) {
|
if (userPublicKey == sender) {
|
||||||
// sender is a linked device
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||||
} else {
|
} else {
|
||||||
@ -322,11 +308,11 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
|||||||
// Unwrap the message
|
// Unwrap the message
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val group = storage.getGroup(groupID) ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.isActive) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
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
|
// Find our wrapper and decrypt it if possible
|
||||||
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
|
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
|
||||||
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
||||||
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||||
// Parse it
|
// Parse it
|
||||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||||
@ -347,7 +333,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
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) {
|
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)
|
// Check that the sender is a member of the group (before the update)
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val group = storage.getGroup(groupID) ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.isActive) {
|
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
|
return
|
||||||
}
|
}
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
@ -375,10 +361,8 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
|||||||
val admins = group.admins.map { it.serialize() }
|
val admins = group.admins.map { it.serialize() }
|
||||||
val name = kind.name
|
val name = kind.name
|
||||||
storage.updateTitle(groupID, name)
|
storage.updateTitle(groupID, name)
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (userPublicKey == senderPublicKey) {
|
if (userPublicKey == senderPublicKey) {
|
||||||
// sender is a linked device
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
@ -395,11 +379,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
|||||||
val groupPublicKey = message.groupPublicKey ?: return
|
val groupPublicKey = message.groupPublicKey ?: return
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val group = storage.getGroup(groupID) ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.isActive) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { 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 updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||||
val newMembers = members + updateMembers
|
val newMembers = members + updateMembers
|
||||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (userPublicKey == senderPublicKey) {
|
if (userPublicKey == senderPublicKey) {
|
||||||
// sender is a linked device
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
|
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
if (userPublicKey in admins) {
|
if (userPublicKey in admins) {
|
||||||
// send current encryption key to the latest added members
|
// Send the latest encryption key pair to the added members if the current user is the admin of the group
|
||||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
//
|
||||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
// 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) {
|
if (encryptionKeyPair == null) {
|
||||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||||
} else {
|
} else {
|
||||||
@ -448,65 +441,54 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
|||||||
val groupPublicKey = message.groupPublicKey ?: return
|
val groupPublicKey = message.groupPublicKey ?: return
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val group = storage.getGroup(groupID) ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.isActive) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
val members = group.members.map { it.serialize() }
|
val members = group.members.map { it.serialize() }
|
||||||
val admins = group.admins.map { it.toString() }
|
val admins = group.admins.map { it.toString() }
|
||||||
|
val removedMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||||
// Users that are part of this remove update
|
|
||||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
|
||||||
|
|
||||||
// Check that the admin wasn't removed
|
// 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.")
|
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the message was sent by the group admin
|
// Check that the message was sent by the group admin
|
||||||
if (!admins.contains(senderPublicKey)) {
|
if (!admins.contains(senderPublicKey)) {
|
||||||
Log.d("Loki", "Ignoring invalid closed group update.")
|
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||||
// If admin leaves the group is disbanded
|
// If the admin leaves the group is disbanded
|
||||||
val didAdminLeave = admins.any { it in updateMembers }
|
val didAdminLeave = admins.any { it in removedMembers }
|
||||||
// newMembers to save is old members minus removed members
|
val newMembers = members - removedMembers
|
||||||
val newMembers = members - updateMembers
|
// A user should be posting a MEMBERS_LEFT in case they leave, so this shouldn't be encountered
|
||||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
val senderLeft = senderPublicKey in removedMembers
|
||||||
val senderLeft = senderPublicKey in updateMembers
|
|
||||||
if (senderLeft) {
|
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
|
val wasCurrentUserRemoved = userPublicKey in removedMembers
|
||||||
|
// Admin should send a MEMBERS_LEFT message but handled here just in case
|
||||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
|
||||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||||
} else {
|
} else {
|
||||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||||
}
|
}
|
||||||
// update zombie members
|
// Update zombie members
|
||||||
val zombies = storage.getZombieMember(groupID)
|
val zombies = storage.getZombieMember(groupID)
|
||||||
storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
|
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||||
|
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT
|
|
||||||
else SignalServiceGroup.Type.MEMBER_REMOVED
|
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
// 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 = updateMembers.minus(zombies)
|
val notificationMembers = removedMembers.minus(zombies)
|
||||||
if (notificationMembers.isNotEmpty()) {
|
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) {
|
if (userPublicKey == senderPublicKey) {
|
||||||
// sender is a linked device
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
|
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
@ -528,11 +510,11 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
val groupPublicKey = message.groupPublicKey ?: return
|
val groupPublicKey = message.groupPublicKey ?: return
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val group = storage.getGroup(groupID) ?: run {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!group.isActive) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val name = group.title
|
val name = group.title
|
||||||
@ -546,19 +528,16 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
val didAdminLeave = admins.contains(senderPublicKey)
|
val didAdminLeave = admins.contains(senderPublicKey)
|
||||||
val updatedMemberList = members - senderPublicKey
|
val updatedMemberList = members - senderPublicKey
|
||||||
val userLeft = (userPublicKey == senderPublicKey)
|
val userLeft = (userPublicKey == senderPublicKey)
|
||||||
|
|
||||||
if (didAdminLeave || userLeft) {
|
if (didAdminLeave || userLeft) {
|
||||||
// admin left the group of linked device left the group
|
|
||||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||||
} else {
|
} else {
|
||||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||||
// update zombie members
|
// Update zombie members
|
||||||
val zombies = storage.getZombieMember(groupID)
|
val zombies = storage.getZombieMember(groupID)
|
||||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||||
}
|
}
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (userLeft) {
|
if (userLeft) {
|
||||||
//sender is a linked device
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
@ -566,9 +545,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidGroupUpdate(group: GroupRecord,
|
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
|
||||||
sentTimestamp: Long,
|
|
||||||
senderPublicKey: String): Boolean {
|
|
||||||
val oldMembers = group.members.map { it.serialize() }
|
val oldMembers = group.members.map { it.serialize() }
|
||||||
// Check that the message isn't from before the group was created
|
// Check that the message isn't from before the group was created
|
||||||
if (group.formationTimestamp > sentTimestamp) {
|
if (group.formationTimestamp > sentTimestamp) {
|
||||||
|
@ -70,11 +70,11 @@ class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val e
|
|||||||
isPollOngoing = true
|
isPollOngoing = true
|
||||||
val server = openGroups.first().server // assume all the same server
|
val server = openGroups.first().server // assume all the same server
|
||||||
val rooms = openGroups.map { it.room }
|
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) ->
|
results.forEach { (room, results) ->
|
||||||
val serverRoomId = "$server.$room"
|
val serverRoomId = "$server.$room"
|
||||||
handleDeletedMessages(serverRoomId,results.deletions)
|
|
||||||
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
||||||
|
handleDeletedMessages(serverRoomId,results.deletions)
|
||||||
}
|
}
|
||||||
}.always {
|
}.always {
|
||||||
isPollOngoing = false
|
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 threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||||
|
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
|
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) ->
|
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||||
|
@ -12,16 +12,15 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
internal object AESGCM {
|
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 gcmTagSize = 128
|
||||||
internal val ivSize = 12
|
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.
|
* 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.FileServerAPI
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
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.utilities.logging.Log
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||||
@ -42,7 +41,7 @@ object DownloadUtilities {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
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 httpUrl = HttpUrl.parse(url)!!
|
||||||
val fileId = httpUrl.pathSegments().last()
|
val fileId = httpUrl.pathSegments().last()
|
||||||
try {
|
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;
|
private final String serialized;
|
||||||
|
|
||||||
|
|
||||||
MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) {
|
MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) {
|
||||||
this.mainColor = mainColor;
|
this.mainColor = mainColor;
|
||||||
this.tintColor = tintColor;
|
this.tintColor = tintColor;
|
||||||
@ -110,9 +109,9 @@ public enum MaterialColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean represents(Context context, int colorValue) {
|
public boolean represents(Context context, int colorValue) {
|
||||||
return context.getResources().getColor(mainColor) == colorValue ||
|
return context.getResources().getColor(mainColor) == colorValue
|
||||||
context.getResources().getColor(tintColor) == colorValue ||
|
|| context.getResources().getColor(tintColor) == colorValue
|
||||||
context.getResources().getColor(shadeColor) == colorValue;
|
|| context.getResources().getColor(shadeColor) == colorValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String serialize() {
|
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;
|
package org.session.libsession.utilities.color.spans;
|
||||||
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.style.MetricAffectingSpan;
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan {
|
public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan {
|
||||||
|
|
||||||
private final float relativeSize;
|
private final float relativeSize;
|
||||||
|
|
||||||
public CenterAlignedRelativeSizeSpan(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.DataMessage;
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
|
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
|
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 org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -51,13 +49,10 @@ public class SignalServiceCipher {
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = SignalServiceCipher.class.getSimpleName();
|
private static final String TAG = SignalServiceCipher.class.getSimpleName();
|
||||||
|
|
||||||
private final SessionProtocol sessionProtocolImpl;
|
|
||||||
private final LokiAPIDatabaseProtocol apiDB;
|
private final LokiAPIDatabaseProtocol apiDB;
|
||||||
|
|
||||||
public SignalServiceCipher(SessionProtocol sessionProtocolImpl,
|
public SignalServiceCipher(LokiAPIDatabaseProtocol apiDB)
|
||||||
LokiAPIDatabaseProtocol apiDB)
|
|
||||||
{
|
{
|
||||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
|
||||||
this.apiDB = apiDB;
|
this.apiDB = apiDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,27 +120,7 @@ public class SignalServiceCipher {
|
|||||||
|
|
||||||
protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException
|
protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException
|
||||||
{
|
{
|
||||||
byte[] paddedMessage;
|
throw new IllegalStateException("This shouldn't be used anymore");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException {
|
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.session.libsignal.service.internal.push.http;
|
package org.session.libsignal.service.internal.push.http;
|
||||||
|
|
||||||
|
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
|
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
|
||||||
import org.session.libsignal.service.api.crypto.DigestingOutputStream;
|
import org.session.libsignal.service.api.crypto.DigestingOutputStream;
|
||||||
|
|
||||||
@ -19,5 +18,4 @@ public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory
|
|||||||
public DigestingOutputStream createFor(OutputStream wrap) throws IOException {
|
public DigestingOutputStream createFor(OutputStream wrap) throws IOException {
|
||||||
return new AttachmentCipherOutputStream(key, wrap);
|
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