mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 13:38:26 +00:00
Merge pull request #441 from hjubb/testing_multi_merge
Testing multi merge
This commit is contained in:
commit
c72c87d698
@ -158,8 +158,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 121
|
||||
def canonicalVersionName = "1.6.4"
|
||||
def canonicalVersionCode = 135
|
||||
def canonicalVersionName = "1.6.12"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
||||
@ -115,7 +116,6 @@ import org.session.libsignal.service.loki.protocol.meta.SessionMetaProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol;
|
||||
|
||||
import java.io.File;
|
||||
@ -206,7 +206,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
||||
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
||||
}
|
||||
MultiDeviceProtocol.Companion.configureIfNeeded(apiDB);
|
||||
org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol.Companion.configureIfNeeded(apiDB);
|
||||
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this);
|
||||
setUpP2PAPIIfNeeded();
|
||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||
@ -249,6 +249,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
startPollingIfNeeded();
|
||||
publicChatManager.markAllAsNotCaughtUp();
|
||||
publicChatManager.startPollersIfNeeded();
|
||||
MultiDeviceProtocol.syncConfigurationIfNeeded(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1109,7 +1109,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
try {
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocolV2.leave(this, groupPublicKey);
|
||||
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey);
|
||||
initializeEnabledCheck();
|
||||
} else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) {
|
||||
initializeEnabledCheck();
|
||||
|
@ -150,6 +150,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public List<GroupRecord> getAllGroups() {
|
||||
Reader reader = getGroups();
|
||||
GroupRecord record;
|
||||
List<GroupRecord> groups = new LinkedList<>();
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (record.isActive()) { groups.add(record); }
|
||||
}
|
||||
reader.close();
|
||||
return groups;
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
||||
List<Address> members = getCurrentMembers(groupId);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
@ -184,7 +195,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
}
|
||||
}
|
||||
|
||||
public void create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
|
||||
public long create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
|
||||
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins)
|
||||
{
|
||||
Collections.sort(members);
|
||||
@ -211,7 +222,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
contentValues.put(ADMINS, Address.Companion.toSerializedList(admins, ','));
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
long threadId = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
Recipient.applyCached(Address.Companion.fromSerialized(groupId), recipient -> {
|
||||
recipient.setName(title);
|
||||
@ -220,6 +231,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
});
|
||||
|
||||
notifyConversationListListeners();
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public boolean delete(@NonNull String groupId) {
|
||||
|
@ -902,6 +902,17 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return insertMessageInbox(retrieved, contentLocation, threadId, type, 0);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertSecureDecryptedMessageOutbox(OutgoingMediaMessage retrieved, long threadId, long serverTimestamp)
|
||||
throws MmsException
|
||||
{
|
||||
long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp);
|
||||
if (messageId == -1) {
|
||||
return Optional.absent();
|
||||
}
|
||||
markAsSent(messageId, true);
|
||||
return Optional.fromNullable(new InsertResult(messageId, threadId));
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId, long serverTimestamp)
|
||||
throws MmsException
|
||||
{
|
||||
|
@ -87,7 +87,7 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
||||
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
|
||||
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
||||
@ -96,8 +96,10 @@ public class MmsSmsDatabase extends Database {
|
||||
MessageRecord messageRecord;
|
||||
|
||||
while ((messageRecord = reader.getNext()) != null) {
|
||||
if ((Util.isOwnNumber(context, author.serialize()) && messageRecord.isOutgoing()) ||
|
||||
(!Util.isOwnNumber(context, author.serialize()) && messageRecord.getIndividualRecipient().getAddress().equals(author)))
|
||||
if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
|
||||
(!Util.isOwnNumber(context, serializedAuthor)
|
||||
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
|
||||
))
|
||||
{
|
||||
return messageRecord;
|
||||
}
|
||||
@ -107,6 +109,10 @@ public class MmsSmsDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
|
||||
return getMessageFor(timestamp, author.serialize());
|
||||
}
|
||||
|
||||
public Cursor getConversation(long threadId, long offset, long limit) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
@ -686,6 +686,15 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp);
|
||||
}
|
||||
|
||||
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) {
|
||||
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null);
|
||||
if (messageId == -1) {
|
||||
return Optional.absent();
|
||||
}
|
||||
markAsSent(messageId, true);
|
||||
return Optional.fromNullable(new InsertResult(messageId, threadId));
|
||||
}
|
||||
|
||||
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||
boolean forceSms, long date, InsertListener insertListener)
|
||||
{
|
||||
@ -718,7 +727,6 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
|
||||
|
||||
if (insertListener != null) {
|
||||
insertListener.onComplete();
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||
@ -27,9 +26,11 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
@ -351,7 +352,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
@ -371,6 +372,38 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun getAllClosedGroupPublicKeys(): Set<String> {
|
||||
return DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
|
||||
}
|
||||
|
||||
override fun addClosedGroupPublicKey(groupPublicKey: String) {
|
||||
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupPublicKey(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun removeClosedGroupPublicKey(groupPublicKey: String) {
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeClosedGroupPublicKey(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
}
|
||||
|
||||
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun getAllOpenGroups(): Map<Long, PublicChat> {
|
||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats()
|
||||
}
|
||||
|
||||
override fun addOpenGroup(server: String, channel: Long) {
|
||||
OpenGroupUtilities.addGroup(context, server, channel)
|
||||
}
|
||||
|
||||
override fun getAllGroups(): List<GroupRecord> {
|
||||
return DatabaseFactory.getGroupDatabase(context).allGroups
|
||||
}
|
||||
|
||||
override fun setProfileSharing(address: Address, value: Boolean) {
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
|
||||
|
@ -194,7 +194,7 @@ public class GroupManager {
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
|
@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
@ -123,11 +125,19 @@ public class GroupMessageProcessor {
|
||||
{
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
|
||||
String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); }
|
||||
|
||||
if (content.getSender().equals(userMasterDevice)) {
|
||||
long threadId = threadDatabase.getThreadIdIfExistsFor(recipient);
|
||||
return threadId == -1 ? null : threadId;
|
||||
}
|
||||
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
|
||||
// Loki - Only update the group if the group admin sent the message
|
||||
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
|
||||
@ -260,9 +270,16 @@ public class GroupMessageProcessor {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
Address senderAddress = Address.Companion.fromExternal(context, content.getSender());
|
||||
MessageRecord existingMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), senderAddress);
|
||||
long messageId;
|
||||
if (existingMessage != null) {
|
||||
messageId = existingMessage.getId();
|
||||
} else {
|
||||
messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
}
|
||||
|
||||
mmsDatabase.markAsSent(messageId, true);
|
||||
|
||||
|
@ -47,7 +47,6 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
@ -80,6 +79,7 @@ import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
@ -96,7 +96,6 @@ import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
@ -264,8 +263,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
|
||||
|
||||
if (content.getDeviceLink().isPresent()) {
|
||||
throw new UnsupportedOperationException("Device link operations are not supported!");
|
||||
if (content.configurationMessageProto.isPresent()) {
|
||||
MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp());
|
||||
} else if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
||||
@ -277,7 +276,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (message.getClosedGroupUpdateV2().isPresent()) {
|
||||
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
|
||||
}
|
||||
|
||||
if (message.isEndSession()) {
|
||||
handleEndSessionMessage(content, smsMessageId);
|
||||
} else if (message.isGroupUpdate()) {
|
||||
@ -398,33 +396,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
|
||||
{
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1);
|
||||
OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
|
||||
if (!recipient.isGroupRecipient()) {
|
||||
// TODO: Handle session reset on sync messages
|
||||
/*
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
sessionStore.deleteAllSessions(recipient.getAddress().toPhoneString());
|
||||
*/
|
||||
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
||||
|
||||
long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage,
|
||||
false, message.getTimestamp(),
|
||||
null);
|
||||
database.markAsSent(messageId, true);
|
||||
}
|
||||
|
||||
return threadId;
|
||||
}
|
||||
|
||||
private void handleGroupMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
@NonNull Optional<Long> smsMessageId)
|
||||
@ -483,98 +454,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeStickerPackOperation(@NonNull List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
for (StickerPackOperationMessage operation : stickerPackOperations) {
|
||||
if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) {
|
||||
String packId = Hex.toStringCondensed(operation.getPackId().get());
|
||||
String packKey = Hex.toStringCondensed(operation.getPackKey().get());
|
||||
|
||||
switch (operation.getType().get()) {
|
||||
case INSTALL:
|
||||
jobManager.add(new StickerPackDownloadJob(packId, packKey, false));
|
||||
break;
|
||||
case REMOVE:
|
||||
DatabaseFactory.getStickerDatabase(context).uninstallPack(packId);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Received incomplete sticker pack operation sync.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SentTranscriptMessage message)
|
||||
throws StorageFailedException
|
||||
|
||||
{
|
||||
try {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
|
||||
Long threadId;
|
||||
|
||||
if (message.getMessage().isEndSession()) {
|
||||
threadId = handleSynchronizeSentEndSessionMessage(message);
|
||||
} else if (message.getMessage().isGroupUpdate()) {
|
||||
threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true);
|
||||
} else if (message.getMessage().isExpirationUpdate()) {
|
||||
threadId = handleSynchronizeSentExpirationUpdate(message);
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message);
|
||||
} else {
|
||||
threadId = handleSynchronizeSentTextMessage(message);
|
||||
}
|
||||
|
||||
if (threadId == -1L) { threadId = null; }
|
||||
|
||||
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) {
|
||||
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
|
||||
}
|
||||
|
||||
if (message.getMessage().getProfileKey().isPresent()) {
|
||||
Recipient recipient = null;
|
||||
|
||||
if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(message.getDestination().get()), false);
|
||||
else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
|
||||
|
||||
|
||||
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
|
||||
}
|
||||
|
||||
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
|
||||
|
||||
if (threadId != null) {
|
||||
DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
|
||||
messageNotifier.updateNotification(context);
|
||||
}
|
||||
|
||||
messageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp());
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleMediaMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
@NonNull Optional<Long> smsMessageId,
|
||||
@NonNull Optional<Long> messageServerIDOrNull)
|
||||
throws StorageFailedException
|
||||
{
|
||||
Recipient originalRecipient = getMessageDestination(content, message);
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
Recipient originalRecipient = getMessageDestination(content, message);
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
String syncTarget = message.getSyncTarget().orNull();
|
||||
|
||||
|
||||
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
|
||||
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
|
||||
Address masterAddress = masterRecipient.getAddress();
|
||||
|
||||
@ -582,75 +478,140 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
|
||||
}
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
|
||||
quote, sharedContacts, linkPreviews, sticker);
|
||||
// Handle sync message from ourselves
|
||||
if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
|
||||
Address targetAddress = masterRecipient.getAddress();
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
|
||||
} else if (syncTarget != null && !syncTarget.isEmpty()) {
|
||||
targetAddress = Address.fromSerialized(syncTarget);
|
||||
}
|
||||
List<Attachment> attachments = PointerAttachment.forPointers(message.getAttachments());
|
||||
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.beginTransaction();
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(),
|
||||
attachments,
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()),
|
||||
linkPreviews.or(Collections.emptyList()),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
// Ignore message if it has no body and no attachments
|
||||
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
|
||||
Log.d("Loki","Message already exists, don't insert again");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InsertResult> insertResult;
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.beginTransaction();
|
||||
|
||||
try {
|
||||
if (message.isGroupMessage()) {
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
|
||||
} else {
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
// Ignore message if it has no body and no attachments
|
||||
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InsertResult> insertResult;
|
||||
|
||||
try {
|
||||
|
||||
// Check if we have the thread already
|
||||
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
|
||||
|
||||
insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp());
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
||||
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
||||
List<DatabaseAttachment> dbAttachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||
|
||||
forceStickerDownloadIfNecessary(stickerAttachments);
|
||||
|
||||
for (DatabaseAttachment attachment : dbAttachments) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
} else {
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
|
||||
quote, sharedContacts, linkPreviews, sticker);
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.beginTransaction();
|
||||
|
||||
// Ignore message if it has no body and no attachments
|
||||
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InsertResult> insertResult;
|
||||
|
||||
try {
|
||||
if (message.isGroupMessage()) {
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
|
||||
} else {
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
||||
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
||||
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||
|
||||
forceStickerDownloadIfNecessary(stickerAttachments);
|
||||
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
||||
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
||||
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||
|
||||
forceStickerDownloadIfNecessary(stickerAttachments);
|
||||
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
InsertResult result = insertResult.get();
|
||||
|
||||
// Loki - Cache the user hex encoded public key (for mentions)
|
||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
||||
|
||||
// Loki - Store message open group server ID if needed
|
||||
if (messageServerIDOrNull.isPresent()) {
|
||||
long messageID = result.getMessageId();
|
||||
long messageServerID = messageServerIDOrNull.get();
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
lokiMessageDatabase.setServerID(messageID, messageServerID);
|
||||
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
}
|
||||
|
||||
// Loki - Update mapping of message ID to original thread ID
|
||||
if (result.getMessageId() > -1) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
||||
if (insertResult.isPresent()) {
|
||||
InsertResult result = insertResult.get();
|
||||
|
||||
// Loki - Cache the user hex encoded public key (for mentions)
|
||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
||||
|
||||
// Loki - Store message open group server ID if needed
|
||||
if (messageServerIDOrNull.isPresent()) {
|
||||
long messageID = result.getMessageId();
|
||||
long messageServerID = messageServerIDOrNull.get();
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
lokiMessageDatabase.setServerID(messageID, messageServerID);
|
||||
}
|
||||
|
||||
// Loki - Update mapping of message ID to original thread ID
|
||||
if (result.getMessageId() > -1) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -769,6 +730,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
||||
Recipient originalRecipient = getMessageDestination(content, message);
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
String syncTarget = message.getSyncTarget().orNull();
|
||||
|
||||
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
|
||||
handleExpirationUpdate(content, message, Optional.absent());
|
||||
@ -778,15 +740,56 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
|
||||
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
|
||||
} else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
|
||||
Address targetAddress = masterRecipient.getAddress();
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
|
||||
} else if (syncTarget != null && !syncTarget.isEmpty()) {
|
||||
targetAddress = Address.fromSerialized(syncTarget);
|
||||
}
|
||||
|
||||
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
|
||||
Log.d("Loki","Message already exists, don't insert again");
|
||||
return;
|
||||
}
|
||||
|
||||
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
|
||||
body, message.getExpiresInSeconds(), -1);
|
||||
|
||||
// Ignore the message if it has no body
|
||||
if (tm.getMessageBody().length() == 0) { return; }
|
||||
|
||||
// Check if we have the thread already
|
||||
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
|
||||
|
||||
|
||||
// Insert the message into the database
|
||||
Optional<InsertResult> insertResult;
|
||||
insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp());
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
threadId = insertResult.get().getThreadId();
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
||||
|
||||
if (threadId != null) {
|
||||
messageNotifier.updateNotification(context, threadId);
|
||||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
InsertResult result = insertResult.get();
|
||||
|
||||
// Loki - Cache the user hex encoded public key (for mentions)
|
||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
||||
}
|
||||
|
||||
} else {
|
||||
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
|
||||
|
||||
Address masterAddress = masterRecipient.getAddress();
|
||||
|
||||
if (message.isGroupMessage()) {
|
||||
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
|
||||
}
|
||||
|
||||
IncomingTextMessage tm = new IncomingTextMessage(masterAddress,
|
||||
content.getSenderDevice(),
|
||||
message.getTimestamp(), body,
|
||||
@ -1319,6 +1322,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) &&
|
||||
DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) {
|
||||
Log.d("Loki", "Skipping message from self we already have");
|
||||
return true;
|
||||
}
|
||||
|
||||
Recipient sender = Recipient.from(context, Address.Companion.fromSerialized(content.getSender()), false);
|
||||
|
||||
if (content.getDeviceLink().isPresent()) {
|
||||
|
@ -15,6 +15,7 @@ import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@ -143,6 +144,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
|
||||
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
|
||||
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
@ -238,25 +242,18 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
// return results;
|
||||
// }
|
||||
|
||||
String groupId = address.toGroupString();
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(addresses)
|
||||
.map(a -> Address.Companion.fromSerialized(a.getNumber()))
|
||||
.map(a -> Recipient.from(context, a, false))
|
||||
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
|
||||
.toList();
|
||||
|
||||
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
|
||||
|
||||
if (message.isGroup() && address.isClosedGroup()) {
|
||||
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
|
||||
String groupId = address.toGroupString();
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
// Loki - Only send GroupUpdate or GroupQuit messages to closed groups
|
||||
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
||||
GroupContext groupContext = groupMessage.getGroupContext();
|
||||
@ -271,25 +268,40 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
|
||||
} else {
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
|
||||
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.asGroupMessage(group)
|
||||
.withAttachments(attachmentPointers)
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSticker(sticker.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.build();
|
||||
SignalServiceDataMessage groupMessage = getDataMessage(address, message).build();
|
||||
|
||||
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) throws IOException {
|
||||
|
||||
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
|
||||
|
||||
String groupId = address.toGroupString();
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.asGroupMessage(group)
|
||||
.withAttachments(attachmentPointers)
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSticker(sticker.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushGroupSendJob> {
|
||||
@Override
|
||||
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
|
@ -245,7 +245,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
{
|
||||
try {
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
@ -254,6 +256,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
@ -267,20 +271,48 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withSyncTarget(destination.serialize())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSticker(sticker.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
|
||||
// Loki - Device link messages don't go through here
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
|
||||
|
||||
messageSender.sendMessage(syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage);
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
return result.getSuccess().isUnidentified();
|
||||
}
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
boolean isUnidentified = result.getSuccess().isUnidentified();
|
||||
|
||||
try {
|
||||
// send to ourselves to sync multi-device
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage);
|
||||
if (selfSendResult.getLokiAPIError() != null) {
|
||||
throw selfSendResult.getLokiAPIError();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki", "Error sending message to ourselves", e);
|
||||
}
|
||||
|
||||
return isUnidentified;
|
||||
}
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, e);
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
@ -192,8 +193,10 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, SnodeAPI.Error
|
||||
{
|
||||
try {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
|
||||
Optional<byte[]> profileKey = getProfileKey(recipient);
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
@ -205,28 +208,49 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
// }
|
||||
|
||||
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
// .withPreKeyBundle(preKeyBundle)
|
||||
.asEndSessionMessage(message.isEndSession())
|
||||
.build();
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.asEndSessionMessage(message.isEndSession())
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withSyncTarget(destination.serialize())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.asEndSessionMessage(message.isEndSession())
|
||||
.build();
|
||||
|
||||
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
|
||||
// Loki - Device link messages don't go through here
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
|
||||
|
||||
messageSender.sendMessage(syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
return result.getSuccess().isUnidentified();
|
||||
}
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
boolean isUnidentified = result.getSuccess().isUnidentified();
|
||||
|
||||
try {
|
||||
// send to ourselves to sync multi-device
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage);
|
||||
if (selfSendResult.getLokiAPIError() != null) {
|
||||
throw selfSendResult.getLokiAPIError();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki", "Error sending message to ourselves", e);
|
||||
}
|
||||
return isUnidentified;
|
||||
}
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
|
@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.loki.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
@ -20,15 +18,13 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import java.lang.ref.WeakReference
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
@ -122,6 +118,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -277,24 +277,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||
ClosedGroupsProtocolV2.leave(this, groupPublicKey!!)
|
||||
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!)
|
||||
} else {
|
||||
// TODO: uncomment when we switch to sending new explicit updates after clients update
|
||||
// task {
|
||||
// val name =
|
||||
// if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name)
|
||||
// else Promise.of(Unit)
|
||||
// name.get()
|
||||
// members.filterNot { it in originalMembers }.let { adds ->
|
||||
// if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
|
||||
// else Promise.of(Unit)
|
||||
// }.get()
|
||||
// originalMembers.filterNot { it in members }.let { removes ->
|
||||
// if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
|
||||
// else Promise.of(Unit)
|
||||
// }.get()
|
||||
// }
|
||||
ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
|
||||
task {
|
||||
val name =
|
||||
if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name)
|
||||
else Promise.of(Unit)
|
||||
name.get()
|
||||
members.filterNot { it in originalMembers }.let { adds ->
|
||||
if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
|
||||
else Promise.of(Unit)
|
||||
}.get()
|
||||
originalMembers.filterNot { it in members }.let { removes ->
|
||||
if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
|
||||
else Promise.of(Unit)
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
promise.successUi {
|
||||
loaderContainer.fadeOut()
|
||||
|
@ -358,7 +358,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
isSSKBasedClosedGroup = false
|
||||
}
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocolV2.leave(context, groupPublicKey!!)
|
||||
ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!)
|
||||
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
@ -73,6 +74,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||
} catch (e: Exception) {
|
||||
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -18,10 +18,11 @@ import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
|
||||
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
@ -47,7 +48,6 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey")
|
||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
val signatureSize = Sign.BYTES
|
||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||
|
||||
|
@ -29,7 +29,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
}
|
||||
|
||||
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
|
||||
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteePublicKey))
|
||||
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
|
||||
return if (message != null) getServerID(message.getId()) else null
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import android.view.LayoutInflater
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
@ -27,6 +28,7 @@ class ClearAllDataDialog : DialogFragment() {
|
||||
|
||||
private fun clearAllData() {
|
||||
if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext())
|
||||
ApplicationContext.getInstance(context).clearAllData(false)
|
||||
} else {
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.jobs.Data
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
@ -128,7 +129,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedContentMessage, false, ttl, false,
|
||||
useFallbackEncryption, false, false, false)
|
||||
useFallbackEncryption, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import org.session.libsession.messaging.jobs.Data
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities
|
||||
@ -26,7 +27,7 @@ import org.session.libsignal.utilities.Hex
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) {
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind, private val sentTime: Long) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
@ -60,20 +61,22 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
}
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind) : this(Parameters.Builder()
|
||||
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(20)
|
||||
.build(),
|
||||
destination,
|
||||
kind)
|
||||
kind,
|
||||
sentTime)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
builder.putLong("sentTime", sentTime)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
@ -123,6 +126,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val sentTime = data.getLong("sentTime")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
@ -161,7 +165,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind)
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,8 +224,8 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedContentMessage, false, ttl, false,
|
||||
true, false, false, false)
|
||||
sentTime, serializedContentMessage, false, ttl, false,
|
||||
true, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
|
||||
}
|
||||
|
@ -285,7 +285,7 @@ object ClosedGroupsProtocol {
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
@ -324,6 +324,6 @@ object ClosedGroupsProtocol {
|
||||
.setId(decodedGroupId)
|
||||
.setType(GroupContext.Type.QUIT)
|
||||
.build()
|
||||
return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, emptyList(), emptyList())
|
||||
return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, 0, null, emptyList(), emptyList())
|
||||
}
|
||||
}
|
@ -38,11 +38,14 @@ import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
|
||||
@ -58,6 +61,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
val sentTime = System.currentTimeMillis()
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
@ -68,20 +72,20 @@ object ClosedGroupsProtocolV2 {
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
if (member == userPublicKey) { continue }
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
@ -103,21 +107,22 @@ object ClosedGroupsProtocolV2 {
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.QUIT
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
@ -141,7 +146,10 @@ object ClosedGroupsProtocolV2 {
|
||||
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) {
|
||||
Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey))
|
||||
}.orNull()
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
return@task Error.NoKeyPair
|
||||
@ -149,21 +157,21 @@ object ClosedGroupsProtocolV2 {
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind)
|
||||
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
|
||||
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +192,7 @@ object ClosedGroupsProtocolV2 {
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
@ -196,17 +205,18 @@ object ClosedGroupsProtocolV2 {
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
return@task Unit
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,21 +229,22 @@ object ClosedGroupsProtocolV2 {
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
@ -273,6 +284,7 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val newMembers = members.minus(oldMembers)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
@ -299,7 +311,7 @@ object ClosedGroupsProtocolV2 {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
if (isUserLeaving) {
|
||||
@ -323,7 +335,7 @@ object ClosedGroupsProtocolV2 {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
@ -336,7 +348,7 @@ object ClosedGroupsProtocolV2 {
|
||||
// Notify the user
|
||||
val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID, sentTime)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
@ -359,7 +371,19 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
|
||||
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, force: Boolean = true) {
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
@ -368,18 +392,20 @@ object ClosedGroupsProtocolV2 {
|
||||
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
|
||||
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
|
||||
}
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers))
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis())
|
||||
if (force) {
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
} else {
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(closedGroupUpdate, senderPublicKey)) { return }
|
||||
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
@ -392,7 +418,10 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean {
|
||||
private fun isValid(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long): Boolean {
|
||||
val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey)
|
||||
if (record != null) return false
|
||||
|
||||
return when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
|
||||
(!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
@ -414,7 +443,7 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
}
|
||||
|
||||
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
@ -427,7 +456,8 @@ object ClosedGroupsProtocolV2 {
|
||||
// Create the group
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
if (groupDB.getGroup(groupID).orNull() != null) {
|
||||
val prevGroup = groupDB.getGroup(groupID).orNull()
|
||||
if (prevGroup != null) {
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
@ -441,8 +471,14 @@ object ClosedGroupsProtocolV2 {
|
||||
// Store the encryption key pair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
// Notify the user (if we didn't make the group)
|
||||
if (userPublicKey != senderPublicKey) {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
} else if (prevGroup == null) {
|
||||
// only notify if we created this group
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
@ -452,8 +488,8 @@ object ClosedGroupsProtocolV2 {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
@ -492,16 +528,22 @@ object ClosedGroupsProtocolV2 {
|
||||
val (contextType, signalType) =
|
||||
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
@ -518,16 +560,32 @@ object ClosedGroupsProtocolV2 {
|
||||
val newMembers = members + updateMembers
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
@ -539,21 +597,23 @@ object ClosedGroupsProtocolV2 {
|
||||
val name = closedGroupUpdate.name
|
||||
groupDB.updateTitle(groupID, name)
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check the user leaving isn't us, will already be handled
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
if (senderPublicKey == userPublicKey) {
|
||||
return
|
||||
}
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
@ -576,7 +636,12 @@ object ClosedGroupsProtocolV2 {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
@ -589,8 +654,8 @@ object ClosedGroupsProtocolV2 {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
@ -624,7 +689,13 @@ object ClosedGroupsProtocolV2 {
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) {
|
||||
@ -700,7 +771,9 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long,
|
||||
sentTime: Long) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
@ -708,9 +781,11 @@ object ClosedGroupsProtocolV2 {
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTime, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
if (mmsSmsDB.getMessageFor(sentTime,userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import java.util.*
|
||||
|
||||
object MultiDeviceProtocol {
|
||||
|
||||
@JvmStatic
|
||||
fun syncConfigurationIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
|
||||
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = recipient(context, userPublicKey)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||
true, false, true, Optional.absent())
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = recipient(context, userPublicKey)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||
true, false, true, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
|
||||
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
if (senderPublicKey != userPublicKey) return
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closedGroup in configurationMessage.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
|
||||
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder()
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
|
||||
closedGroupUpdate.name = closedGroup.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
|
||||
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in configurationMessage.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup)) continue
|
||||
OpenGroupUtilities.addGroup(context, openGroup, 1)
|
||||
}
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities
|
||||
@ -56,7 +57,7 @@ class NullMessageSendJob private constructor(parameters: Parameters, private val
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedContentMessage, false, ttl, false,
|
||||
false, false, false, false)
|
||||
false, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.")
|
||||
throw e
|
||||
|
@ -14,7 +14,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object KeyPairUtilities {
|
||||
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun generate(): KeyPairGenerationResult {
|
||||
val seed = sodium.randomBytesBuf(16)
|
||||
|
@ -27,7 +27,7 @@ object OpenGroupUtilities {
|
||||
val groupID = PublicChat.getId(channel, url)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
if (openGroup != null) return openGroup
|
||||
if (openGroup != null) { return openGroup }
|
||||
|
||||
// Add the new group.
|
||||
val application = ApplicationContext.getInstance(context)
|
||||
|
@ -40,7 +40,23 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTimeMillis,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@ -48,7 +64,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
sentTime,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
|
@ -326,7 +326,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
||||
|
||||
private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) {
|
||||
String publicKey = recipient.getAddress().serialize();
|
||||
String hepk = (recipient.isLocalNumber() && publicKey != null)
|
||||
String hepk = (recipient.isLocalNumber() && publicKey == null)
|
||||
? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
|
||||
: publicKey;
|
||||
String displayName = recipient.getName();
|
||||
|
@ -6,12 +6,7 @@ public class IncomingEncryptedMessage extends IncomingTextMessage {
|
||||
super(base, newBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingTextMessage withMessageBody(String body) {
|
||||
return new IncomingEncryptedMessage(this, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public boolean isSecureMessage() {
|
||||
return true;
|
||||
}
|
||||
|
@ -10,12 +10,7 @@ public class IncomingEndSessionMessage extends IncomingTextMessage {
|
||||
super(base, newBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingEndSessionMessage withMessageBody(String messageBody) {
|
||||
return new IncomingEndSessionMessage(this, messageBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public boolean isEndSession() {
|
||||
return true;
|
||||
}
|
||||
|
@ -11,12 +11,7 @@ public class IncomingGroupMessage extends IncomingTextMessage {
|
||||
this.groupContext = groupContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingGroupMessage withMessageBody(String body) {
|
||||
return new IncomingGroupMessage(this, groupContext, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
@ -9,12 +9,7 @@ public class IncomingPreKeyBundleMessage extends IncomingTextMessage {
|
||||
this.legacy = legacy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingPreKeyBundleMessage withMessageBody(String messageBody) {
|
||||
return new IncomingPreKeyBundleMessage(this, messageBody, legacy);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public boolean isLegacyPreKeyBundle() {
|
||||
return legacy;
|
||||
}
|
||||
|
@ -175,10 +175,6 @@ public class IncomingTextMessage implements Parcelable {
|
||||
return message;
|
||||
}
|
||||
|
||||
public IncomingTextMessage withMessageBody(String message) {
|
||||
return new IncomingTextMessage(this, message);
|
||||
}
|
||||
|
||||
public Address getSender() {
|
||||
return sender;
|
||||
}
|
||||
@ -250,7 +246,6 @@ public class IncomingTextMessage implements Parcelable {
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
|
@ -21,6 +21,7 @@ import org.session.libsignal.libsignal.ecc.ECPrivateKey
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
|
||||
interface StorageProtocol {
|
||||
|
||||
@ -56,6 +57,8 @@ interface StorageProtocol {
|
||||
// Open Groups
|
||||
fun getOpenGroup(threadID: String): OpenGroup?
|
||||
fun getThreadID(openGroupID: String): String?
|
||||
fun getAllOpenGroups(): Map<Long, PublicChat>
|
||||
fun addOpenGroup(server: String, channel: Long)
|
||||
|
||||
// Open Group Public Keys
|
||||
fun getOpenGroupPublicKey(server: String): String?
|
||||
@ -65,6 +68,13 @@ interface StorageProtocol {
|
||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
||||
|
||||
// Open Group Metadata
|
||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||
fun updateTitle(groupID: String, newValue: String)
|
||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||
|
||||
// Last Message Server ID
|
||||
fun getLastMessageServerID(group: Long, server: String): Long?
|
||||
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
||||
@ -75,13 +85,6 @@ interface StorageProtocol {
|
||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||
fun removeLastDeletionServerID(group: Long, server: String)
|
||||
|
||||
// Open Group Metadata
|
||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||
fun updateTitle(groupID: String, newValue: String)
|
||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||
|
||||
// Message Handling
|
||||
fun getReceivedMessageTimestamps(): Set<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
@ -101,6 +104,11 @@ interface StorageProtocol {
|
||||
fun removeMember(groupID: String, member: Address)
|
||||
fun updateMembers(groupID: String, members: List<Address>)
|
||||
// Closed Group
|
||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||
fun removeClosedGroupPublicKey(groupPublicKey: String)
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
@ -109,6 +117,9 @@ interface StorageProtocol {
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(): List<GroupRecord>
|
||||
|
||||
// Settings
|
||||
fun setProfileSharing(address: Address, value: Boolean)
|
||||
|
||||
|
@ -6,6 +6,8 @@ import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
@ -33,11 +35,23 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
class NameChange(val name: String) : Kind()
|
||||
class MembersAdded(val members: List<ByteString>) : Kind()
|
||||
class MembersRemoved( val members: List<ByteString>) : Kind()
|
||||
class MemberLeft() : Kind()
|
||||
object MemberLeft : Kind()
|
||||
|
||||
val description: String = run {
|
||||
when(this) {
|
||||
is New -> "new"
|
||||
is Update -> "update"
|
||||
is EncryptionKeyPair -> "encryptionKeyPair"
|
||||
is NameChange -> "nameChange"
|
||||
is MembersAdded -> "membersAdded"
|
||||
is MembersRemoved -> "membersRemoved"
|
||||
MemberLeft -> "memberLeft"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ClosedGroupUpdateV2"
|
||||
const val TAG = "ClosedGroupControlMessage"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||
val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null
|
||||
@ -75,7 +89,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList)
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> {
|
||||
kind = Kind.MemberLeft()
|
||||
kind = Kind.MemberLeft
|
||||
}
|
||||
}
|
||||
return ClosedGroupControlMessage(kind)
|
||||
@ -168,10 +182,15 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
final class KeyPairWrapper(val publicKey: String?, private val encryptedKeyPair: ByteString?) {
|
||||
class KeyPairWrapper(val publicKey: String?, val encryptedKeyPair: ByteString?) {
|
||||
|
||||
val isValid: Boolean = run {
|
||||
this.publicKey != null && this.encryptedKeyPair != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair)
|
||||
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +198,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val publicKey = publicKey ?: return null
|
||||
val encryptedKeyPair = encryptedKeyPair ?: return null
|
||||
val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(publicKey.toByteArray())
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = encryptedKeyPair
|
||||
|
||||
return try {
|
||||
|
@ -0,0 +1,106 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>): ControlMessage() {
|
||||
|
||||
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.ClosedGroup): ClosedGroup? {
|
||||
if (!proto.hasPublicKey() || !proto.hasName() || !proto.hasEncryptionKeyPair()) return null
|
||||
val publicKey = proto.publicKey.toByteArray().toHexString()
|
||||
val name = proto.name
|
||||
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.ConfigurationMessage.ClosedGroup? {
|
||||
val result = SignalServiceProtos.ConfigurationMessage.ClosedGroup.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.name = name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
override val ttl: Long = 4 * 24 * 60 * 60 * 1000
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
|
||||
fun getCurrent(): ConfigurationMessage {
|
||||
val closedGroups = mutableListOf<ClosedGroup>()
|
||||
val openGroups = mutableListOf<String>()
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groups = storage.getAllGroups()
|
||||
for (groupRecord in groups) {
|
||||
if (groupRecord.isClosedGroup) {
|
||||
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.getDecodedGroupIDAsData(groupRecord.encodedId).toHexString()
|
||||
if (!storage.isClosedGroup(groupPublicKey)) continue
|
||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (groupRecord.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||
val openGroup = storage.getOpenGroup(threadID) ?: continue
|
||||
openGroups.add(openGroup.server)
|
||||
}
|
||||
}
|
||||
return ConfigurationMessage(closedGroups, openGroups)
|
||||
}
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ConfigurationMessage? {
|
||||
if (!proto.hasConfigurationMessage()) return null
|
||||
val configurationProto = proto.configurationMessage
|
||||
val closedGroups = configurationProto.closedGroupsList.mapNotNull { ClosedGroup.fromProto(it) }
|
||||
val openGroups = configurationProto.openGroupsList
|
||||
return ConfigurationMessage(closedGroups, openGroups)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
|
||||
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
|
||||
configurationProto.addAllOpenGroups(openGroups)
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
contentProto.configurationMessage = configurationProto.build()
|
||||
return contentProto.build()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return """
|
||||
ConfigurationMessage(
|
||||
closedGroups: ${(closedGroups)}
|
||||
openGroups: ${(openGroups)}
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
@ -4,12 +4,8 @@ import android.text.TextUtils
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
@ -17,19 +13,19 @@ import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPrevie
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
@ -45,8 +41,9 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
when (message) {
|
||||
is ReadReceipt -> handleReadReceipt(message)
|
||||
is TypingIndicator -> handleTypingIndicator(message)
|
||||
is ClosedGroupUpdate -> handleClosedGroupUpdate(message)
|
||||
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
|
||||
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto)
|
||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
||||
}
|
||||
}
|
||||
@ -105,6 +102,21 @@ fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, proto
|
||||
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(id, senderPublicKey, proto)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
if (message.sender != storage.getUserPublicKey()) return
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closeGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in message.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val context = MessagingConfiguration.shared.context
|
||||
@ -188,173 +200,293 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
SSKEnvironment.shared.notificationManager.updateNotification(context, threadID)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) {
|
||||
private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroupControlMessage) {
|
||||
when (message.kind!!) {
|
||||
is ClosedGroupUpdate.Kind.New -> handleNewGroup(message)
|
||||
is ClosedGroupUpdate.Kind.Info -> handleGroupUpdate(message)
|
||||
is ClosedGroupUpdate.Kind.SenderKeyRequest -> handleSenderKeyRequest(message)
|
||||
is ClosedGroupUpdate.Kind.SenderKey -> handleSenderKey(message)
|
||||
is ClosedGroupControlMessage.Kind.New -> handleNewClosedGroup(message)
|
||||
is ClosedGroupControlMessage.Kind.Update -> handleClosedGroupUpdated(message)
|
||||
is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> handleClosedGroupEncryptionKeyPair(message)
|
||||
is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message)
|
||||
is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message)
|
||||
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
|
||||
ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
||||
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins)
|
||||
}
|
||||
|
||||
// Parameter @sender:String is just for inserting incoming info message
|
||||
private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
if (message.kind !is ClosedGroupUpdate.Kind.New) { return }
|
||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.New
|
||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||
val name = kind.name
|
||||
val groupPrivateKey = kind.groupPrivateKey
|
||||
val senderKeys = kind.senderKeys
|
||||
val members = kind.members.map { it.toHexString() }
|
||||
val admins = kind.admins.map { it.toHexString() }
|
||||
// Persist the ratchets
|
||||
senderKeys.forEach { senderKey ->
|
||||
if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach }
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
|
||||
}
|
||||
// Sort out any discrepancies between the provided sender keys and what's required
|
||||
val missingSenderKeys = members.toSet().subtract(senderKeys.map { Hex.toStringCondensed(it.publicKey) })
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
if (missingSenderKeys.contains(userPublicKey)) {
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
members.forEach { member ->
|
||||
if (member == userPublicKey) return@forEach
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(groupPublicKey.toByteArray(), userSenderKey)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey))
|
||||
}
|
||||
}
|
||||
missingSenderKeys.minus(userPublicKey).forEach { publicKey ->
|
||||
MessageSender.requestSenderKey(groupPublicKey, publicKey)
|
||||
}
|
||||
// Create the group
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
if (storage.getGroup(groupID) != null) {
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
storage.insertIncomingInfoMessage(context, message.sender!!, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) {
|
||||
private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControlMessage) {
|
||||
// Prepare
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
if (message.kind !is ClosedGroupUpdate.Kind.Info) { return }
|
||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.Info
|
||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||
val name = kind.name
|
||||
val senderKeys = kind.senderKeys
|
||||
val members = kind.members.map { it.toHexString() }
|
||||
val admins = kind.admins.map { it.toHexString() }
|
||||
// Get the group
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
|
||||
val group = storage.getGroup(groupID) ?: return Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (!group.members.contains(Address.fromSerialized(message.sender!!))) { return Log.d("Loki", "Ignoring closed group info message from non-member.") }
|
||||
// Store the ratchets for any new members (it's important that this happens before the code below)
|
||||
senderKeys.forEach { senderKey ->
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
|
||||
}
|
||||
// Delete all ratchets and either:
|
||||
// • Send out the user's new ratchet using established channels if other members of the group left or were removed
|
||||
// • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.Update ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val wasUserRemoved = !members.contains(userPublicKey)
|
||||
val wasSenderRemoved = !members.contains(message.sender!!)
|
||||
if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) {
|
||||
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
for (pair in allOldRatchets) {
|
||||
val senderPublicKey = pair.first
|
||||
val ratchet = pair.second
|
||||
val collection = ClosedGroupRatchetCollectionType.Old
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection)
|
||||
}
|
||||
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
if (wasUserRemoved) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
} else {
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
members.forEach { member ->
|
||||
if (member == userPublicKey) return@forEach
|
||||
val address = Address.fromSerialized(member)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
}
|
||||
}
|
||||
// Unwrap the message
|
||||
val name = kind.name
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// Check that the admin wasn't removed unless the group was destroyed entirely
|
||||
if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) {
|
||||
android.util.Log.d("Loki", "Ignoring invalid closed group update message.")
|
||||
return
|
||||
}
|
||||
// Remove the group from the user's set of public keys to poll for if the current user was removed
|
||||
val wasCurrentUserRemoved = !members.contains(userPublicKey)
|
||||
if (wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
}
|
||||
// Generate and distribute a new encryption key pair if needed
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
// Notify the user if needed
|
||||
if (!wasCurrentUserRemoved) {
|
||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
storage.insertIncomingInfoMessage(context, message.sender!!, groupID, type0, type1, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleSenderKeyRequest(message: ClosedGroupUpdate) {
|
||||
if (message.kind !is ClosedGroupUpdate.Kind.SenderKeyRequest) { return }
|
||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKeyRequest
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
||||
// Prepare
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
|
||||
val group = storage.getGroup(groupID)
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.")
|
||||
val userKeyPair = storage.getUserX25519KeyPair()
|
||||
// Unwrap the message
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
// Check that the requesting user is a member of the group
|
||||
if (!group.members.map { it.serialize() }.contains(message.sender!!)) {
|
||||
Log.d("Loki", "Ignoring closed group sender key request from non-member.")
|
||||
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
|
||||
return
|
||||
}
|
||||
// Respond to the request
|
||||
Log.d("Loki", "Responding to sender key request from: ${message.sender!!}.")
|
||||
val userRatchet = sskDatabase.getClosedGroupRatchet(groupPublicKey, userPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
?: SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
MessageSender.send(closedGroupUpdate, Address.fromSerialized(groupID))
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = kind.wrappers.firstOrNull { it.publicKey!!.toByteArray().toHexString() == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
||||
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
// Store it
|
||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleSenderKey(message: ClosedGroupUpdate) {
|
||||
if (message.kind !is ClosedGroupUpdate.Kind.SenderKey) { return }
|
||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKey
|
||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||
val senderKey = kind.senderKey
|
||||
if (senderKey.publicKey.toHexString() != message.sender!!) {
|
||||
Log.d("Loki", "Ignoring invalid closed group sender key.")
|
||||
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
Log.d("Loki", "Received a sender key from: ${message.sender!!}.")
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
MessagingConfiguration.shared.sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members + updateMembers
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersRemoved ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in updateMembers }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members - updateMembers
|
||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
||||
val senderLeft = senderPublicKey in updateMembers
|
||||
if (senderLeft) {
|
||||
android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in updateMembers
|
||||
|
||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
if (senderPublicKey == userPublicKey) { return } // Check the user leaving isn't us, will already be handled
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
|
||||
if (didAdminLeave) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
sentTimestamp: Long,
|
||||
senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.createdAt > sentTimestamp) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
return false
|
||||
}
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (senderPublicKey !in oldMembers) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
}
|
@ -41,6 +41,7 @@ object MessageSender {
|
||||
|
||||
// Closed groups
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key.")
|
||||
object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
|
||||
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
|
||||
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
||||
|
||||
|
@ -3,15 +3,19 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
@ -21,56 +25,193 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSende
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import java.util.*
|
||||
|
||||
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
// Prepare
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val members = members
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
// Generate a key pair for the group
|
||||
val groupKeyPair = Curve.generateKeyPair()
|
||||
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
|
||||
members.plus(userPublicKey)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
// Create ratchets for all members
|
||||
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey ->
|
||||
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
|
||||
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val membersAsData = members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
if (member == userPublicKey) { continue }
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get()
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupID)
|
||||
}
|
||||
// Create the group
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members using established channels
|
||||
val promises = mutableListOf<Promise<Unit, Exception>>()
|
||||
for (member in members) {
|
||||
if (member == userPublicKey) { continue }
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
|
||||
senderKeys, membersAsData, adminsAsData)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(member)
|
||||
val promise = MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||
promises.add(promise)
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Notify the user
|
||||
val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupPublicKey)
|
||||
// Return
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun MessageSender.v2_update(groupPublicKey: String, members: List<String>, name: String) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
// Update name if needed
|
||||
if (name != group.title) { setName(groupPublicKey, name) }
|
||||
// Add members if needed
|
||||
val addedMembers = members - group.members.map { it.serialize() }
|
||||
if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) }
|
||||
// Remove members if needed
|
||||
val removedMembers = group.members.map { it.serialize() } - members
|
||||
if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) }
|
||||
}
|
||||
|
||||
fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't change name for nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, newName)
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
|
||||
}
|
||||
|
||||
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't add members to nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
if (membersToAdd.isEmpty()) {
|
||||
Log.d("Loki", "Invalid closed group update.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
val membersAsData = updatedMembers.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val newMembersAsData = membersToAdd.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: run {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
}
|
||||
|
||||
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't remove members from nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
if (membersToRemove.isEmpty()) {
|
||||
Log.d("Loki", "Invalid closed group update.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
}
|
||||
|
||||
fun MessageSender.v2_leave(groupPublicKey: String) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
}
|
||||
|
||||
fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val context = MessagingConfiguration.shared.context
|
||||
@ -216,11 +357,34 @@ fun MessageSender.leave(groupPublicKey: String) {
|
||||
return update(groupPublicKey, newMembers, name).get()
|
||||
}
|
||||
|
||||
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) {
|
||||
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
// Prepare
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
|
||||
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// Distribute it
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.map { publicKey ->
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(wrappers)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.*
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
@ -152,6 +152,7 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
val UNKNOWN = Address("Unknown")
|
||||
private val TAG = Address::class.java.simpleName
|
||||
private val cachedFormatter = AtomicReference<Pair<String, ExternalAddressFormatter>>()
|
||||
@JvmStatic
|
||||
fun fromSerialized(serialized: String): Address {
|
||||
return Address(serialized)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair
|
||||
|
||||
object UnidentifiedAccessUtil {
|
||||
private val TAG = UnidentifiedAccessUtil::class.simpleName
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? {
|
||||
try {
|
||||
|
@ -2,6 +2,8 @@ package org.session.libsession.utilities
|
||||
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object GroupUtil {
|
||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||
@ -58,11 +60,27 @@ object GroupUtil {
|
||||
return groupId.startsWith(MMS_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isOpenGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isClosedGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
// NOTE: Signal group ID handling is weird. The ID is double encoded in the database, but not in a `GroupContext`.
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun doubleEncodeGroupID(groupPublicKey: String): String {
|
||||
return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun doubleDecodeGroupID(groupID: String): ByteArray {
|
||||
return getDecodedGroupIDAsData(getDecodedGroupID(groupID))
|
||||
}
|
||||
}
|
@ -128,6 +128,40 @@ object TextSecurePreferences {
|
||||
private const val FCM_TOKEN = "pref_fcm_token"
|
||||
private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
|
||||
// region Multi Device
|
||||
private const val IS_USING_MULTI_DEVICE = "pref_is_using_multi_device"
|
||||
private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||
private const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingMultiDevice(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_USING_MULTI_DEVICE, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setIsUsingMultiDevice(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_USING_MULTI_DEVICE, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setLastConfigurationSyncTime(context: Context, value: Long) {
|
||||
setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getConfigurationMessageSynced(context: Context): Boolean {
|
||||
return getBooleanPreference(context, CONFIGURATION_SYNCED, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setConfigurationMessageSynced(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, CONFIGURATION_SYNCED, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingFCM(context: Context): Boolean {
|
||||
|
@ -26,10 +26,6 @@ import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.calls.AnswerMessage;
|
||||
import org.session.libsignal.service.api.messages.calls.IceUpdateMessage;
|
||||
import org.session.libsignal.service.api.messages.calls.OfferMessage;
|
||||
import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.BlockedListMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.ReadMessage;
|
||||
@ -51,7 +47,6 @@ import org.session.libsignal.service.internal.push.PushServiceSocket;
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.CallMessage;
|
||||
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.GroupContext;
|
||||
@ -217,34 +212,14 @@ public class SignalServiceMessageSender {
|
||||
* @param recipient The sender of the received message you're acknowledging.
|
||||
* @param message The read receipt to deliver.
|
||||
* @throws IOException
|
||||
* @throws UntrustedIdentityException
|
||||
*/
|
||||
public void sendReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
SignalServiceReceiptMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
throws IOException {
|
||||
byte[] content = createReceiptContent(message);
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a typing indicator.
|
||||
*
|
||||
* @param recipient The destination
|
||||
* @param message The typing indicator to deliver
|
||||
* @throws IOException
|
||||
* @throws UntrustedIdentityException
|
||||
*/
|
||||
public void sendTyping(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createTypingContent(message);
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), useFallbackEncryption, false);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
|
||||
}
|
||||
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
@ -256,42 +231,24 @@ public class SignalServiceMessageSender {
|
||||
sendMessage(0, recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a call setup message to a single recipient.
|
||||
*
|
||||
* @param recipient The message's destination.
|
||||
* @param message The call message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void sendCallMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
SignalServiceCallMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createCallContent(message);
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, message.getTTL(), useFallbackEncryption, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a single recipient.
|
||||
*
|
||||
* @param recipient The message's destination.
|
||||
* @param message The message.
|
||||
* @throws UntrustedIdentityException
|
||||
* @throws IOException
|
||||
*/
|
||||
public SendMessageResult sendMessage(long messageID,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
SignalServiceDataMessage message)
|
||||
throws UntrustedIdentityException, IOException
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createMessageContent(message, recipient);
|
||||
long timestamp = message.getTimestamp();
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
|
||||
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
|
||||
SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, false, message.hasVisibleContent());
|
||||
SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, message.hasVisibleContent(), message.getSyncTarget());
|
||||
|
||||
// // Loki - This shouldn't get invoked for note to self
|
||||
// boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent();
|
||||
@ -325,8 +282,7 @@ public class SignalServiceMessageSender {
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SignalServiceDataMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
throws IOException {
|
||||
// Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine
|
||||
// whether an attachment is being sent to an open group or not.
|
||||
byte[] content = createMessageContent(message, recipients.get(0));
|
||||
@ -350,7 +306,7 @@ public class SignalServiceMessageSender {
|
||||
for (String device : linkedDevices) {
|
||||
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(syncMessage, device, store);
|
||||
sendMessage(deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption, true);
|
||||
sendMessage(deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption);
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,7 +337,6 @@ public class SignalServiceMessageSender {
|
||||
} else if (message.getStickerPackOperations().isPresent()) {
|
||||
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
sendMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
return;
|
||||
} else {
|
||||
throw new IOException("Unsupported sync message!");
|
||||
@ -392,18 +347,10 @@ public class SignalServiceMessageSender {
|
||||
for (String device : linkedDevices) {
|
||||
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store);
|
||||
sendMessageToPrivateChat(0, deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false);
|
||||
sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false, Optional.absent());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSoTimeoutMillis(long soTimeoutMillis) {
|
||||
socket.setSoTimeoutMillis(soTimeoutMillis);
|
||||
}
|
||||
|
||||
public void cancelInFlightRequests() {
|
||||
socket.cancelInFlightRequests();
|
||||
}
|
||||
|
||||
public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) {
|
||||
this.pipe.set(Optional.fromNullable(pipe));
|
||||
this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe));
|
||||
@ -452,12 +399,6 @@ public class SignalServiceMessageSender {
|
||||
result.getUrl());
|
||||
}
|
||||
|
||||
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private byte[] createTypingContent(SignalServiceTypingMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
TypingMessage.Builder builder = TypingMessage.newBuilder();
|
||||
@ -494,32 +435,6 @@ public class SignalServiceMessageSender {
|
||||
{
|
||||
Content.Builder container = Content.newBuilder();
|
||||
|
||||
// if (message.getPreKeyBundle().isPresent()) {
|
||||
// PreKeyBundle preKeyBundle = message.getPreKeyBundle().get();
|
||||
// PreKeyBundleMessage.Builder preKeyBundleMessageBuilder = PreKeyBundleMessage.newBuilder()
|
||||
// .setDeviceId(preKeyBundle.getDeviceId())
|
||||
// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize()))
|
||||
// .setPreKeyId(preKeyBundle.getPreKeyId())
|
||||
// .setPreKey(ByteString.copyFrom(preKeyBundle.getPreKey().serialize()))
|
||||
// .setSignedKeyId(preKeyBundle.getSignedPreKeyId())
|
||||
// .setSignedKey(ByteString.copyFrom(preKeyBundle.getSignedPreKey().serialize()))
|
||||
// .setSignature(ByteString.copyFrom(preKeyBundle.getSignedPreKeySignature()))
|
||||
// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize()));
|
||||
// container.setPreKeyBundleMessage(preKeyBundleMessageBuilder);
|
||||
// }
|
||||
|
||||
// if (message.getDeviceLink().isPresent()) {
|
||||
// DeviceLink deviceLink = message.getDeviceLink().get();
|
||||
// SignalServiceProtos.DeviceLinkMessage.Builder deviceLinkMessageBuilder = SignalServiceProtos.DeviceLinkMessage.newBuilder()
|
||||
// .setPrimaryPublicKey(deviceLink.getMasterPublicKey())
|
||||
// .setSecondaryPublicKey(deviceLink.getSlavePublicKey())
|
||||
// .setRequestSignature(ByteString.copyFrom(Objects.requireNonNull(deviceLink.getRequestSignature())));
|
||||
// if (deviceLink.getAuthorizationSignature() != null) {
|
||||
// deviceLinkMessageBuilder.setAuthorizationSignature(ByteString.copyFrom(deviceLink.getAuthorizationSignature()));
|
||||
// }
|
||||
// container.setDeviceLinkMessage(deviceLinkMessageBuilder.build());
|
||||
// }
|
||||
|
||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
|
||||
|
||||
@ -559,6 +474,10 @@ public class SignalServiceMessageSender {
|
||||
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
|
||||
}
|
||||
|
||||
if (message.getSyncTarget().isPresent()) {
|
||||
builder.setSyncTarget(message.getSyncTarget().get());
|
||||
}
|
||||
|
||||
if (message.getQuote().isPresent()) {
|
||||
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
|
||||
.setId(message.getQuote().get().getId())
|
||||
@ -636,40 +555,6 @@ public class SignalServiceMessageSender {
|
||||
return container.build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createCallContent(SignalServiceCallMessage callMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
CallMessage.Builder builder = CallMessage.newBuilder();
|
||||
|
||||
if (callMessage.getOfferMessage().isPresent()) {
|
||||
OfferMessage offer = callMessage.getOfferMessage().get();
|
||||
builder.setOffer(CallMessage.Offer.newBuilder()
|
||||
.setId(offer.getId())
|
||||
.setDescription(offer.getDescription()));
|
||||
} else if (callMessage.getAnswerMessage().isPresent()) {
|
||||
AnswerMessage answer = callMessage.getAnswerMessage().get();
|
||||
builder.setAnswer(CallMessage.Answer.newBuilder()
|
||||
.setId(answer.getId())
|
||||
.setDescription(answer.getDescription()));
|
||||
} else if (callMessage.getIceUpdateMessages().isPresent()) {
|
||||
List<IceUpdateMessage> updates = callMessage.getIceUpdateMessages().get();
|
||||
|
||||
for (IceUpdateMessage update : updates) {
|
||||
builder.addIceUpdate(CallMessage.IceUpdate.newBuilder()
|
||||
.setId(update.getId())
|
||||
.setSdp(update.getSdp())
|
||||
.setSdpMid(update.getSdpMid())
|
||||
.setSdpMLineIndex(update.getSdpMLineIndex()));
|
||||
}
|
||||
} else if (callMessage.getHangupMessage().isPresent()) {
|
||||
builder.setHangup(CallMessage.Hangup.newBuilder().setId(callMessage.getHangupMessage().get().getId()));
|
||||
} else if (callMessage.getBusyMessage().isPresent()) {
|
||||
builder.setBusy(CallMessage.Busy.newBuilder().setId(callMessage.getBusyMessage().get().getId()));
|
||||
}
|
||||
|
||||
container.setCallMessage(builder);
|
||||
return container.build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete)
|
||||
throws IOException
|
||||
{
|
||||
@ -987,6 +872,7 @@ public class SignalServiceMessageSender {
|
||||
throws IOException
|
||||
{
|
||||
List<SendMessageResult> results = new LinkedList<>();
|
||||
SignalServiceAddress ownAddress = localAddress;
|
||||
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
|
||||
|
||||
@ -995,7 +881,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
try {
|
||||
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(content, recipient.getNumber(), store);
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, false, notifyPNServer);
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, notifyPNServer, Optional.absent());
|
||||
results.add(result);
|
||||
} catch (UnregisteredUserException e) {
|
||||
Log.w(TAG, e);
|
||||
@ -1009,41 +895,46 @@ public class SignalServiceMessageSender {
|
||||
return results;
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isSyncMessage)
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption)
|
||||
throws IOException
|
||||
{
|
||||
// Loki - This method is only invoked for various types of control messages
|
||||
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, isSyncMessage, false);
|
||||
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, false,Optional.absent());
|
||||
}
|
||||
|
||||
public SendMessageResult sendMessage(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
public SendMessageResult sendMessage(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean isDeviceLinkMessage,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
boolean isSyncMessage,
|
||||
boolean notifyPNServer)
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean isDeviceLinkMessage,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
throws IOException
|
||||
{
|
||||
long threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty();
|
||||
long threadID;
|
||||
if (isSelfSend) {
|
||||
threadID = threadDatabase.getThreadID(syncTarget.get());
|
||||
} else {
|
||||
threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
}
|
||||
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
|
||||
try {
|
||||
if (publicChat != null) {
|
||||
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
|
||||
} else {
|
||||
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer);
|
||||
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget);
|
||||
}
|
||||
} catch (PushNetworkException e) {
|
||||
return SendMessageResult.networkFailure(recipient);
|
||||
@ -1152,10 +1043,10 @@ public class SignalServiceMessageSender {
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
final boolean notifyPNServer)
|
||||
final boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); }
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
@ -1221,14 +1112,10 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(exception -> {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
@ -1304,12 +1191,6 @@ public class SignalServiceMessageSender {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
|
||||
throws IOException
|
||||
{
|
||||
return createAttachmentPointer(attachment, false, null);
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
|
@ -216,7 +216,18 @@ public class SignalServiceCipher {
|
||||
);
|
||||
}
|
||||
|
||||
if (message.hasDeviceLinkMessage()) {
|
||||
if (message.hasConfigurationMessage()) {
|
||||
SignalServiceCipher.Metadata metadata = plaintext.getMetadata();
|
||||
SignalServiceContent content = new SignalServiceContent(message, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp());
|
||||
|
||||
if (message.hasDataMessage()) {
|
||||
setProfile(message.getDataMessage(), content);
|
||||
SignalServiceDataMessage signalServiceDataMessage = createSignalServiceMessage(metadata, message.getDataMessage());
|
||||
content.setDataMessage(signalServiceDataMessage);
|
||||
}
|
||||
|
||||
return content;
|
||||
} else if (message.hasDeviceLinkMessage()) {
|
||||
SignalServiceProtos.DeviceLinkMessage protoDeviceLinkMessage = message.getDeviceLinkMessage();
|
||||
String masterPublicKey = protoDeviceLinkMessage.getPrimaryPublicKey();
|
||||
String slavePublicKey = protoDeviceLinkMessage.getSecondaryPublicKey();
|
||||
@ -331,7 +342,6 @@ public class SignalServiceCipher {
|
||||
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
|
||||
paddedMessage = plaintextAndSenderPublicKey.getFirst();
|
||||
String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
|
||||
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob
|
||||
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
|
||||
sessionVersion = sessionCipher.getSessionVersion();
|
||||
} else if (envelope.isPreKeySignalMessage()) {
|
||||
@ -389,6 +399,7 @@ public class SignalServiceCipher {
|
||||
ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate();
|
||||
ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2();
|
||||
boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0);
|
||||
String syncTarget = content.getSyncTarget();
|
||||
|
||||
for (AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||
attachments.add(createAttachmentPointer(pointer));
|
||||
@ -417,7 +428,8 @@ public class SignalServiceCipher {
|
||||
null,
|
||||
closedGroupUpdate,
|
||||
closedGroupUpdateV2,
|
||||
isDeviceUnlinkingRequest);
|
||||
isDeviceUnlinkingRequest,
|
||||
syncTarget);
|
||||
}
|
||||
|
||||
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)
|
||||
|
@ -12,7 +12,9 @@ import org.session.libsignal.service.api.messages.SignalServiceNullMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage;
|
||||
|
||||
@ -33,10 +35,11 @@ public class SignalServiceContent {
|
||||
private final Optional<SignalServiceTypingMessage> typingMessage;
|
||||
|
||||
// Loki
|
||||
private final Optional<DeviceLink> deviceLink;
|
||||
public Optional<PreKeyBundleMessage> preKeyBundleMessage = Optional.absent();
|
||||
public Optional<String> senderDisplayName = Optional.absent();
|
||||
public Optional<String> senderProfilePictureURL = Optional.absent();
|
||||
private final Optional<DeviceLink> deviceLink;
|
||||
public Optional<SignalServiceProtos.Content> configurationMessageProto = Optional.absent();
|
||||
public Optional<PreKeyBundleMessage> preKeyBundleMessage = Optional.absent();
|
||||
public Optional<String> senderDisplayName = Optional.absent();
|
||||
public Optional<String> senderProfilePictureURL = Optional.absent();
|
||||
|
||||
public SignalServiceContent(SignalServiceDataMessage message, String sender, int senderDevice, long timestamp, boolean needsReceipt, boolean isDeviceUnlinkingRequest) {
|
||||
this.sender = sender;
|
||||
@ -128,6 +131,22 @@ public class SignalServiceContent {
|
||||
this.isDeviceUnlinkingRequest = false;
|
||||
}
|
||||
|
||||
public SignalServiceContent(SignalServiceProtos.Content configurationMessageProto, String sender, int senderDevice, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
this.timestamp = timestamp;
|
||||
this.needsReceipt = false;
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.nullMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.deviceLink = Optional.absent();
|
||||
this.configurationMessageProto = Optional.fromNullable(configurationMessageProto);
|
||||
this.isDeviceUnlinkingRequest = false;
|
||||
}
|
||||
|
||||
public SignalServiceContent(SignalServiceNullMessage nullMessage, String sender, int senderDevice, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
|
@ -41,6 +41,7 @@ public class SignalServiceDataMessage {
|
||||
private final Optional<ClosedGroupUpdate> closedGroupUpdate;
|
||||
private final Optional<ClosedGroupUpdateV2> closedGroupUpdateV2;
|
||||
private final boolean isDeviceUnlinkingRequest;
|
||||
private final Optional<String> syncTarget;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceDataMessage with a body and no attachments.
|
||||
@ -134,7 +135,7 @@ public class SignalServiceDataMessage {
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker)
|
||||
{
|
||||
this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false);
|
||||
this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,7 +156,7 @@ public class SignalServiceDataMessage {
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink,
|
||||
ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2,
|
||||
boolean isDeviceUnlinkingRequest)
|
||||
boolean isDeviceUnlinkingRequest, String syncTarget)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.body = Optional.fromNullable(body);
|
||||
@ -172,6 +173,7 @@ public class SignalServiceDataMessage {
|
||||
this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate);
|
||||
this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2);
|
||||
this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest;
|
||||
this.syncTarget = Optional.fromNullable(syncTarget);
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
this.attachments = Optional.of(attachments);
|
||||
@ -250,6 +252,10 @@ public class SignalServiceDataMessage {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public Optional<String> getSyncTarget() {
|
||||
return syncTarget;
|
||||
}
|
||||
|
||||
public Optional<Quote> getQuote() {
|
||||
return quote;
|
||||
}
|
||||
@ -307,6 +313,7 @@ public class SignalServiceDataMessage {
|
||||
private Sticker sticker;
|
||||
private PreKeyBundle preKeyBundle;
|
||||
private DeviceLink deviceLink;
|
||||
private String syncTarget;
|
||||
private boolean isDeviceUnlinkingRequest;
|
||||
|
||||
private Builder() {}
|
||||
@ -336,6 +343,11 @@ public class SignalServiceDataMessage {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSyncTarget(String syncTarget) {
|
||||
this.syncTarget = syncTarget;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder asEndSessionMessage() {
|
||||
return asEndSessionMessage(true);
|
||||
}
|
||||
@ -417,7 +429,7 @@ public class SignalServiceDataMessage {
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
sticker, preKeyBundle, deviceLink,
|
||||
null, null,
|
||||
isDeviceUnlinkingRequest);
|
||||
isDeviceUnlinkingRequest, syncTarget);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,19 +65,20 @@ object DownloadUtilities {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
val input = body.inputStream()
|
||||
val buffer = ByteArray(32768)
|
||||
var count = 0
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
outputStream.write(buffer, 0, bytes)
|
||||
count += bytes
|
||||
if (count > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
body.inputStream().use { input ->
|
||||
val buffer = ByteArray(32768)
|
||||
var count = 0
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
outputStream.write(buffer, 0, bytes)
|
||||
count += bytes
|
||||
if (count > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
||||
bytes = input.read(buffer)
|
||||
}
|
||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
||||
bytes = input.read(buffer)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Couldn't download attachment due to error: $e.")
|
||||
|
Loading…
x
Reference in New Issue
Block a user