Merge remote-tracking branch 'upstream/dev' into open_groups_V2

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
#	app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
#	app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt
#	app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt
#	app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt
#	app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt
#	libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt
#	libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt
#	libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt
#	libsignal/src/main/java/org/session/libsignal/service/loki/Mention.kt
#	libsignal/src/main/java/org/session/libsignal/service/loki/utilities/mentions/Mention.kt
This commit is contained in:
jubb 2021-05-03 15:37:53 +10:00
commit fa528c47d5
67 changed files with 319 additions and 286 deletions

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms
import android.util.Log
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.jvm.asDispatcher
import org.session.libsignal.utilities.ThreadUtils
import java.util.concurrent.Executors
object AppContext {
fun configureKovenant() {
Kovenant.context {
callbackContext.dispatcher = Executors.newSingleThreadExecutor().asDispatcher()
workerContext.dispatcher = ThreadUtils.executorPool.asDispatcher()
multipleCompletion = { v1, v2 ->
Log.d("Loki", "Promise resolved more than once (first with $v1, then with $v2); ignoring $v2.")
}
}
}
}

View File

@ -49,7 +49,7 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWr
import org.session.libsession.utilities.dynamiclanguage.LocaleParser; import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.api.util.StreamDetails;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
@ -93,7 +93,6 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.util.Date; import java.util.Date;
@ -104,6 +103,7 @@ import dagger.ObjectGraph;
import kotlin.Unit; import kotlin.Unit;
import kotlinx.coroutines.Job; import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import nl.komponents.kovenant.Kovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant; import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
@ -164,6 +164,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
ProcessLifecycleOwner.get().getLifecycle().addObserver(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
// Loki // Loki
// ======== // ========
AppContext.INSTANCE.configureKovenant();
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier()); messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
broadcaster = new Broadcaster(this); broadcaster = new Broadcaster(this);
threadNotificationHandler = new Handler(Looper.getMainLooper()); threadNotificationHandler = new Handler(Looper.getMainLooper());

View File

@ -84,6 +84,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
} }
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
keyCachingService.setMasterSecret(new Object());
keyCachingService = null; keyCachingService = null;
} }
}, Context.BIND_AUTO_CREATE); }, Context.BIND_AUTO_CREATE);
@ -133,7 +134,9 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
private void handleAuthenticated() { private void handleAuthenticated() {
authenticated = true; authenticated = true;
//TODO Replace with a proper call. //TODO Replace with a proper call.
keyCachingService.setMasterSecret(new Object()); if (keyCachingService != null) {
keyCachingService.setMasterSecret(new Object());
}
// Finish and proceed with the next intent. // Finish and proceed with the next intent.
Intent nextIntent = getIntent().getParcelableExtra("next_intent"); Intent nextIntent = getIntent().getParcelableExtra("next_intent");
@ -188,6 +191,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
if (!keyguardManager.isKeyguardSecure()) { if (!keyguardManager.isKeyguardSecure()) {
Log.w(TAG ,"Keyguard not secure..."); Log.w(TAG ,"Keyguard not secure...");
TextSecurePreferences.setScreenLockEnabled(getApplicationContext(), false);
TextSecurePreferences.setScreenLockTimeout(getApplicationContext(), 0);
handleAuthenticated(); handleAuthenticated();
return; return;
} }

View File

@ -109,10 +109,10 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil; import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.AssertedSuccessListener; import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsession.utilities.mentions.Mention;
import org.session.libsession.utilities.views.Stub; import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.Mention;
import org.session.libsignal.service.loki.utilities.HexEncodingKt; import org.session.libsignal.service.loki.utilities.HexEncodingKt;
import org.session.libsignal.service.loki.utilities.PublicKeyValidation; import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
import org.session.libsignal.utilities.concurrent.ListenableFuture; import org.session.libsignal.utilities.concurrent.ListenableFuture;

View File

@ -25,10 +25,9 @@ import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol; import org.session.libsignal.service.loki.LokiOpenGroupDatabaseProtocol;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -44,6 +43,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
static final String GROUP_ID = "group_id"; static final String GROUP_ID = "group_id";
private static final String TITLE = "title"; private static final String TITLE = "title";
private static final String MEMBERS = "members"; private static final String MEMBERS = "members";
private static final String ZOMBIE_MEMBERS = "zombie_members";
private static final String AVATAR = "avatar"; private static final String AVATAR = "avatar";
private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_KEY = "avatar_key";
@ -64,6 +64,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
GROUP_ID + " TEXT, " + GROUP_ID + " TEXT, " +
TITLE + " TEXT, " + TITLE + " TEXT, " +
MEMBERS + " TEXT, " + MEMBERS + " TEXT, " +
ZOMBIE_MEMBERS + " TEXT, " +
AVATAR + " BLOB, " + AVATAR + " BLOB, " +
AVATAR_ID + " INTEGER, " + AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " + AVATAR_KEY + " BLOB, " +
@ -81,7 +82,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}; };
private static final String[] GROUP_PROJECTION = { private static final String[] GROUP_PROJECTION = {
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, GROUP_ID, TITLE, MEMBERS, ZOMBIE_MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS
}; };
@ -162,7 +163,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) { public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
List<Address> members = getCurrentMembers(groupId); List<Address> members = getCurrentMembers(groupId, false);
List<Recipient> recipients = new LinkedList<>(); List<Recipient> recipients = new LinkedList<>();
for (Address member : members) { for (Address member : members) {
@ -177,6 +178,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return recipients; return recipients;
} }
public @NonNull List<Recipient> getGroupZombieMembers(String groupId) {
List<Address> members = getCurrentZombieMembers(groupId);
List<Recipient> recipients = new LinkedList<>();
for (Address member : members) {
recipients.add(Recipient.from(context, member, false));
}
return recipients;
}
public long 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, @NonNull Long formationTimestamp) @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins, @NonNull Long formationTimestamp)
{ {
@ -300,6 +312,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}); });
} }
public void updateZombieMembers(String groupId, List<Address> members) {
Collections.sort(members);
ContentValues contents = new ContentValues();
contents.put(ZOMBIE_MEMBERS, Address.toSerializedList(members, ','));
contents.put(ACTIVE, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId});
}
public void updateAdmins(String groupId, List<Address> admins) { public void updateAdmins(String groupId, List<Address> admins) {
Collections.sort(admins); Collections.sort(admins);
@ -311,7 +333,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
public void removeMember(String groupId, Address source) { public void removeMember(String groupId, Address source) {
List<Address> currentMembers = getCurrentMembers(groupId); List<Address> currentMembers = getCurrentMembers(groupId, false);
currentMembers.remove(source); currentMembers.remove(source);
ContentValues contents = new ContentValues(); ContentValues contents = new ContentValues();
@ -329,18 +351,22 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}); });
} }
private List<Address> getCurrentMembers(String groupId) { private List<Address> getCurrentMembers(String groupId, boolean zombieMembers) {
Cursor cursor = null; Cursor cursor = null;
String membersColumn = MEMBERS;
if (zombieMembers) membersColumn = ZOMBIE_MEMBERS;
try { try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS}, cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {membersColumn},
GROUP_ID + " = ?", GROUP_ID + " = ?",
new String[] {groupId}, new String[] {groupId},
null, null, null); null, null, null);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)); String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(membersColumn));
return Address.fromSerializedList(serializedMembers, ','); if (serializedMembers != null && !serializedMembers.isEmpty())
return Address.fromSerializedList(serializedMembers, ',');
} }
return new LinkedList<>(); return new LinkedList<>();
@ -350,6 +376,10 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
} }
private List<Address> getCurrentZombieMembers(String groupId) {
return getCurrentMembers(groupId, true);
}
public boolean isActive(String groupId) { public boolean isActive(String groupId) {
Optional<GroupRecord> record = getGroup(groupId); Optional<GroupRecord> record = getGroup(groupId);
return record.isPresent() && record.get().isActive(); return record.isPresent() && record.get().isActive();

View File

@ -455,6 +455,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value) DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
} }
override fun getZombieMember(groupID: String): Set<String> {
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
}
override fun removeMember(groupID: String, member: Address) { override fun removeMember(groupID: String, member: Address) {
DatabaseFactory.getGroupDatabase(context).removeMember(groupID, member) DatabaseFactory.getGroupDatabase(context).removeMember(groupID, member)
} }
@ -463,6 +467,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
} }
override fun updateZombieMembers(groupID: String, members: List<Address>) {
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
}
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) { override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)

View File

@ -276,6 +276,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
} }
if (oldVersion < lokiV23) { if (oldVersion < lokiV23) {
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType()); db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable()); db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
} }

View File

@ -37,8 +37,14 @@ import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>() private val originalMembers = HashSet<String>()
private val zombies = HashSet<String>()
private val members = HashSet<String>() private val members = HashSet<String>()
private val allMembers: Set<String>
get() {
return members + zombies
}
private var hasNameChanged = false private var hasNameChanged = false
private var isSelfAdmin = false
private var isLoading = false private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() } set(newValue) { field = newValue; invalidateOptionsMenu() }
@ -54,7 +60,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
private val memberListAdapter by lazy { private val memberListAdapter by lazy {
EditClosedGroupMembersAdapter(this, GlideApp.with(this), this::onMemberClick) if (isSelfAdmin)
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin, this::onMemberClick)
else
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin)
} }
private lateinit var mainContentContainer: LinearLayout private lateinit var mainContentContainer: LinearLayout
@ -81,7 +90,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable)) ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!! groupID = intent.getStringExtra(groupIDKey)!!
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title val groupInfo = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get()
originalName = groupInfo.title
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
name = originalName name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer) mainContentContainer = findViewById(R.id.mainContentContainer)
@ -116,31 +128,35 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> { LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<GroupMembers> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<GroupMembers> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID) return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
} }
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) { override fun onLoadFinished(loader: Loader<GroupMembers>, groupMembers: GroupMembers) {
// We no longer need any subsequent loading events // We no longer need any subsequent loading events
// (they will occur on every activity resume). // (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID) LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
members.clear()
members.addAll(groupMembers.members.toHashSet())
zombies.clear()
zombies.addAll(groupMembers.zombieMembers.toHashSet())
originalMembers.clear() originalMembers.clear()
originalMembers.addAll(members.toHashSet()) originalMembers.addAll(members + zombies)
updateMembers(originalMembers) updateMembers()
} }
override fun onLoaderReset(loader: Loader<List<String>>) { override fun onLoaderReset(loader: Loader<GroupMembers>) {
updateMembers(setOf()) updateMembers()
} }
}) })
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu) menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return members.isNotEmpty() && !isLoading return allMembers.isNotEmpty() && !isLoading
} }
// endregion // endregion
@ -153,8 +169,8 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet() val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet()
val changedMembers = members + selectedContacts members.addAll(selectedContacts)
updateMembers(changedMembers) updateMembers()
} }
} }
} }
@ -173,17 +189,12 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun updateMembers(members: Set<String>) { private fun updateMembers() {
this.members.clear() memberListAdapter.setMembers(allMembers)
this.members.addAll(members) memberListAdapter.setZombieMembers(zombies)
memberListAdapter.setMembers(members)
val admins = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().admins.map { it.toString() }.toMutableSet() mainContentContainer.visibility = if (allMembers.isEmpty()) View.GONE else View.VISIBLE
admins.remove(TextSecurePreferences.getLocalNumber(this)) emptyStateContainer.visibility = if (allMembers.isEmpty()) View.VISIBLE else View.GONE
memberListAdapter.setLockedMembers(admins)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -200,8 +211,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onMemberClick(member: String) { private fun onMemberClick(member: String) {
val bottomSheet = ClosedGroupEditingOptionsBottomSheet() val bottomSheet = ClosedGroupEditingOptionsBottomSheet()
bottomSheet.onRemoveTapped = { bottomSheet.onRemoveTapped = {
val changedMembers = members - member if (zombies.contains(member)) zombies.remove(member)
updateMembers(changedMembers) else members.remove(member)
updateMembers()
bottomSheet.dismiss() bottomSheet.dismiss()
} }
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet") bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
@ -209,7 +221,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onAddMembersClick() { private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java) val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.toTypedArray()) intent.putExtra(SelectContactsActivity.usersToExcludeKey, allMembers.toTypedArray())
intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add") intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode) startActivityForResult(intent, addUsersRequestCode)
} }
@ -229,7 +241,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
private fun commitChanges() { private fun commitChanges() {
val hasMemberListChanges = (members != originalMembers) val hasMemberListChanges = (allMembers != originalMembers)
if (!hasNameChanged && !hasMemberListChanges) { if (!hasNameChanged && !hasMemberListChanges) {
return finish() return finish()
@ -237,15 +249,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
val name = if (hasNameChanged) this.name else originalName val name = if (hasNameChanged) this.name else originalName
val members = this.members.map { val members = this.allMembers.map {
Recipient.from(this, Address.fromSerialized(it), false) Recipient.from(this, Address.fromSerialized(it), false)
}.toSet() }.toSet()
val originalMembers = this.originalMembers.map { val originalMembers = this.originalMembers.map {
Recipient.from(this, Address.fromSerialized(it), false) Recipient.from(this, Address.fromSerialized(it), false)
}.toSet() }.toSet()
val admins = members.toSet() //TODO For now, consider all the users to be admins.
var isClosedGroup: Boolean var isClosedGroup: Boolean
var groupPublicKey: String? var groupPublicKey: String?
try { try {
@ -303,4 +313,6 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
} }
class GroupMembers(val members: List<String>, val zombieMembers: List<String>) { }
} }

View File

@ -4,12 +4,19 @@ import android.content.Context
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.AsyncLoader import org.thoughtcrime.securesms.util.AsyncLoader
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<List<String>>(context) { class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<EditClosedGroupActivity.GroupMembers>(context) {
override fun loadInBackground(): List<String> { override fun loadInBackground(): EditClosedGroupActivity.GroupMembers {
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true) val groupDatabase = DatabaseFactory.getGroupDatabase(context)
return members.map { val members = groupDatabase.getGroupMembers(groupID, true)
it.address.toString() val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
} return EditClosedGroupActivity.GroupMembers(
members.map {
it.address.toString()
},
zombieMembers.map {
it.address.toString()
}
)
} }
} }

View File

@ -7,15 +7,17 @@ import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.loki.views.UserView import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
class EditClosedGroupMembersAdapter( class EditClosedGroupMembersAdapter(
private val context: Context, private val context: Context,
private val glide: GlideRequests, private val glide: GlideRequests,
private val admin: Boolean,
private val memberClickListener: ((String) -> Unit)? = null private val memberClickListener: ((String) -> Unit)? = null
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() { ) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
private val members = ArrayList<String>() private val members = ArrayList<String>()
private val lockedMembers = HashSet<String>() private val zombieMembers = ArrayList<String>()
fun setMembers(members: Collection<String>) { fun setMembers(members: Collection<String>) {
this.members.clear() this.members.clear()
@ -23,9 +25,9 @@ class EditClosedGroupMembersAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
fun setLockedMembers(members: Collection<String>) { fun setZombieMembers(members: Collection<String>) {
this.lockedMembers.clear() this.zombieMembers.clear()
this.lockedMembers.addAll(members) this.zombieMembers.addAll(members)
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -39,15 +41,20 @@ class EditClosedGroupMembersAdapter(
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val member = members[position] val member = members[position]
val lockedMember = lockedMembers.contains(member) val unlocked = admin && member != TextSecurePreferences.getLocalNumber(context)
viewHolder.view.bind(Recipient.from( viewHolder.view.bind(Recipient.from(
context, context,
Address.fromSerialized(member), false), Address.fromSerialized(member), false),
glide, glide,
if (lockedMember) UserView.ActionIndicator.None else UserView.ActionIndicator.Menu) if (unlocked) UserView.ActionIndicator.Menu else UserView.ActionIndicator.None)
if (!lockedMember) { if (zombieMembers.contains(member))
viewHolder.view.alpha = 0.5F
else
viewHolder.view.alpha = 1F
if (unlocked) {
viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) } viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) }
} }
} }

View File

@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
@ -45,7 +45,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
private var restoreJob: Job? = null private var restoreJob: Job? = null
override fun onBackPressed() { override fun onBackPressed() {
if (restoreJob?.isActive == true) return // don't allow going back with pending job if (restoreJob?.isActive == true) return // Don't allow going back with a pending job
super.onBackPressed() super.onBackPressed()
} }
@ -53,14 +53,12 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@LinkDeviceActivity, true) setHasViewedSeed(this@LinkDeviceActivity, true)
setConfigurationMessageSynced(this@LinkDeviceActivity, false) setConfigurationMessageSynced(this@LinkDeviceActivity, false)
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@LinkDeviceActivity, 0) setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
} }
// registration variables are synced
setContentView(R.layout.activity_link_device) setContentView(R.layout.activity_link_device)
viewPager.adapter = adapter viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) tabLayout.setupWithViewPager(viewPager)

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
@ -30,14 +30,12 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true) setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false) setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
} }
// registration variables are synced
setContentView(R.layout.activity_recovery_phrase_restore) setContentView(R.layout.activity_recovery_phrase_restore)
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }

View File

@ -39,14 +39,12 @@ class RegisterActivity : BaseActionBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register) setContentView(R.layout.activity_register)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@RegisterActivity, false) setHasViewedSeed(this@RegisterActivity, false)
setConfigurationMessageSynced(this@RegisterActivity, true) setConfigurationMessageSynced(this@RegisterActivity, true)
setRestorationTime(this@RegisterActivity, 0) setRestorationTime(this@RegisterActivity, 0)
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
} }
// registration variables are synced
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
copyButton.setOnClickListener { copyPublicKey() } copyButton.setOnClickListener { copyPublicKey() }
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")

View File

@ -16,7 +16,7 @@ import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
class SeedActivity : BaseActionBarActivity() { class SeedActivity : BaseActionBarActivity() {

View File

@ -8,7 +8,7 @@ import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation import org.session.libsignal.service.loki.utilities.PublicKeyValidation
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString

View File

@ -2,12 +2,11 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {

View File

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol import org.session.libsignal.service.loki.LokiUserDatabaseProtocol
class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol { class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol {

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.dialog_seed.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender

View File

@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.loki.utilities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.session.libsignal.service.loki.Broadcaster
class Broadcaster(private val context: Context) : org.session.libsignal.service.loki.utilities.Broadcaster { class Broadcaster(private val context: Context) : Broadcaster {
override fun broadcast(event: String) { override fun broadcast(event: String) {
val intent = Intent(event) val intent = Intent(event)

View File

@ -1,10 +1,6 @@
package org.thoughtcrime.securesms.loki.utilities package org.thoughtcrime.securesms.loki.utilities
import android.content.Context import android.content.Context
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
object MnemonicUtilities { object MnemonicUtilities {

View File

@ -7,10 +7,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ListView import android.widget.ListView
import org.session.libsession.utilities.mentions.Mention
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsignal.service.loki.Mention
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var mentionCandidates = listOf<Mention>() private var mentionCandidates = listOf<Mention>()

View File

@ -9,7 +9,7 @@ import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.* import kotlinx.android.synthetic.main.view_mention_candidate.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.utilities.mentions.Mention import org.session.libsignal.service.loki.Mention
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {

View File

@ -146,13 +146,13 @@ class ProfilePictureView : RelativeLayout {
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
if (publicKey.isNotEmpty()) { if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return if (imagesCached.contains(publicKey)) return
val signalProfilePicture = recipient.contactPhoto val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.clear(imageView) glide.clear(imageView)
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
imagesCached.add(recipient.profileAvatar.orEmpty()) imagesCached.add(publicKey)
} else { } else {
val sizeInPX = resources.getDimensionPixelSize(sizeResId) val sizeInPX = resources.getDimensionPixelSize(sizeResId)
glide.clear(imageView) glide.clear(imageView)
@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout {
publicKey, publicKey,
displayName displayName
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
imagesCached.add(recipient.profileAvatar.orEmpty()) imagesCached.add(publicKey)
} }
} else { } else {
imageView.setImageDrawable(null) imageView.setImageDrawable(null)

View File

@ -102,7 +102,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
if (duration == 0) { if (duration == 0) {
TextSecurePreferences.setScreenLockTimeout(getContext(), 0); TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
} else { } else {
long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60); long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration);
// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds); TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
} }

View File

@ -119,7 +119,6 @@ public class KeyCachingService extends Service {
KeyCachingService.masterSecret = masterSecret; KeyCachingService.masterSecret = masterSecret;
foregroundService(); foregroundService();
startTimeoutIfAppropriate(this);
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@Override @Override
@ -210,7 +209,7 @@ public class KeyCachingService extends Service {
boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context); boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context);
long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context); long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context);
boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context); boolean screenLockActive = screenTimeout >= 0 && TextSecurePreferences.isScreenLockEnabled(context);
if (!appVisible && secretSet && (passLockActive || screenLockActive)) { if (!appVisible && secretSet && (passLockActive || screenLockActive)) {
long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context); long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context);

View File

@ -98,13 +98,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/home_activity_gradient" /> android:background="@drawable/home_activity_gradient" />
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
<LinearLayout <LinearLayout
android:id="@+id/emptyStateContainer" android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -131,6 +124,13 @@
</LinearLayout> </LinearLayout>
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@ -110,8 +110,10 @@ interface StorageProtocol {
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long) fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
fun isGroupActive(groupPublicKey: String): Boolean fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean) fun setActive(groupID: String, value: Boolean)
fun getZombieMember(groupID: String): Set<String>
fun removeMember(groupID: String, member: Address) fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>) fun updateMembers(groupID: String, members: List<Address>)
fun updateZombieMembers(groupID: String, members: List<Address>)
// Closed Group // Closed Group
fun getAllClosedGroupPublicKeys(): Set<String> fun getAllClosedGroupPublicKeys(): Set<String>
fun getAllActiveClosedGroupPublicKeys(): Set<String> fun getAllActiveClosedGroupPublicKeys(): Set<String>

View File

@ -8,7 +8,7 @@ import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.* import org.session.libsignal.service.loki.utilities.*
import java.net.URL import java.net.URL

View File

@ -15,7 +15,7 @@ import org.session.libsignal.service.internal.crypto.PaddingInputStream
import org.session.libsignal.service.internal.push.PushAttachmentData import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {

View File

@ -1,9 +1,9 @@
package org.session.libsession.messaging.mentions package org.session.libsession.messaging.mentions
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.mentions.Mention import org.session.libsignal.service.loki.Mention
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol import org.session.libsignal.service.loki.LokiUserDatabaseProtocol
class MentionsManager(private val userPublicKey: String, private val userDatabase: LokiUserDatabaseProtocol) { class MentionsManager(private val userPublicKey: String, private val userDatabase: LokiUserDatabaseProtocol) {
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys

View File

@ -145,7 +145,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
} }
is Kind.EncryptionKeyPair -> { is Kind.EncryptionKeyPair -> {
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
closedGroupControlMessage.publicKey = kind.publicKey closedGroupControlMessage.publicKey = kind.publicKey ?: ByteString.EMPTY
closedGroupControlMessage.addAllWrappers(kind.wrappers.map { it.toProto() }) closedGroupControlMessage.addAllWrappers(kind.wrappers.map { it.toProto() })
} }
is Kind.NameChange -> { is Kind.NameChange -> {

View File

@ -21,7 +21,6 @@ import java.util.*
object OpenGroupAPI: DotNetAPI() { object OpenGroupAPI: DotNetAPI() {
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
val sharedContext = Kovenant.createContext()
// region Settings // region Settings
private val fallbackBatchCount = 64 private val fallbackBatchCount = 64
@ -61,7 +60,7 @@ object OpenGroupAPI: DotNetAPI() {
parameters["count"] = fallbackBatchCount parameters["count"] = fallbackBatchCount
parameters["include_deleted"] = 0 parameters["include_deleted"] = 0
} }
return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json -> return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then { json ->
try { try {
val data = json["data"] as List<Map<*, *>> val data = json["data"] as List<Map<*, *>>
val messages = data.mapNotNull { message -> val messages = data.mapNotNull { message ->
@ -166,7 +165,7 @@ object OpenGroupAPI: DotNetAPI() {
} else { } else {
parameters["count"] = fallbackBatchCount parameters["count"] = fallbackBatchCount
} }
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then(sharedContext) { json -> return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then { json ->
try { try {
val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion -> val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion ->
try { try {
@ -202,7 +201,7 @@ object OpenGroupAPI: DotNetAPI() {
retryIfNeeded(maxRetryCount) { retryIfNeeded(maxRetryCount) {
Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.") Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.")
val parameters = signedMessage.toJSON() val parameters = signedMessage.toJSON()
execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json -> execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then { json ->
try { try {
val data = json["data"] as Map<*, *> val data = json["data"] as Map<*, *>
val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong() val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong()
@ -255,7 +254,7 @@ object OpenGroupAPI: DotNetAPI() {
@JvmStatic @JvmStatic
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> { fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json -> return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then { json ->
try { try {
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String> @Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
val moderatorsAsSet = moderators.orEmpty().toSet() val moderatorsAsSet = moderators.orEmpty().toSet()
@ -276,7 +275,7 @@ object OpenGroupAPI: DotNetAPI() {
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> { fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
val parameters = mapOf( "include_annotations" to 1 ) val parameters = mapOf( "include_annotations" to 1 )
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then(sharedContext) { json -> execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then { json ->
try { try {
val data = json["data"] as Map<*, *> val data = json["data"] as Map<*, *>
val annotations = data["annotations"] as List<Map<*, *>> val annotations = data["annotations"] as List<Map<*, *>>
@ -357,7 +356,7 @@ object OpenGroupAPI: DotNetAPI() {
@JvmStatic @JvmStatic
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> { fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json -> return getUserProfiles(publicKeys, server, false).map { json ->
val mapping = mutableMapOf<String, String>() val mapping = mutableMapOf<String, String>()
for (user in json) { for (user in json) {
if (user["username"] != null) { if (user["username"] != null) {

View File

@ -17,16 +17,13 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.GroupUtil import org.session.libsignal.service.loki.HTTP
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.service.loki.HTTP.Verb.*
import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.api.utilities.HTTP.Verb.*
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Base64.* import org.session.libsignal.utilities.Base64.*
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.createContext
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import java.util.* import java.util.*
@ -39,7 +36,6 @@ object OpenGroupAPIV2 {
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1) val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
private val sharedContext = Kovenant.createContext()
private val curve = Curve25519.getInstance(Curve25519.BEST) private val curve = Curve25519.getInstance(Curve25519.BEST)
sealed class Error : Exception() { sealed class Error : Exception() {
@ -128,8 +124,8 @@ object OpenGroupAPIV2 {
} }
when (request.verb) { when (request.verb) {
GET -> requestBuilder.get() GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.parameters)) PUT -> requestBuilder.put(createBody(request.parameters)!!)
POST -> requestBuilder.post(createBody(request.parameters)) POST -> requestBuilder.post(createBody(request.parameters)!!)
DELETE -> requestBuilder.delete(createBody(request.parameters)) DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
@ -156,7 +152,7 @@ object OpenGroupAPIV2 {
} }
} }
return if (request.isAuthRequired) { return if (request.isAuthRequired) {
getAuthToken(request.room!!, request.server).bind(sharedContext) { execute(it) } getAuthToken(request.room!!, request.server).bind { execute(it) }
} else { } else {
execute(null) execute(null)
} }
@ -164,7 +160,7 @@ object OpenGroupAPIV2 {
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> { fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false) val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val result = json["result"] as? String ?: throw Error.PARSING_FAILED val result = json["result"] as? String ?: throw Error.PARSING_FAILED
decode(result) decode(result)
} }
@ -176,7 +172,7 @@ object OpenGroupAPIV2 {
Promise.of(it) Promise.of(it)
} ?: run { } ?: run {
requestNewAuthToken(room, server) requestNewAuthToken(room, server)
.bind(sharedContext) { claimAuthToken(it, room, server) } .bind { claimAuthToken(it, room, server) }
.success { authToken -> .success { authToken ->
storage.setAuthToken(room, server, authToken) storage.setAuthToken(room, server, authToken)
} }
@ -188,7 +184,7 @@ object OpenGroupAPIV2 {
?: return Promise.ofFail(Error.GENERIC) ?: return Promise.ofFail(Error.GENERIC)
val queryParameters = mutableMapOf("public_key" to publicKey) val queryParameters = mutableMapOf("public_key" to publicKey)
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null) val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
val base64EncodedCiphertext = challenge["ciphertext"] as? String val base64EncodedCiphertext = challenge["ciphertext"] as? String
?: throw Error.PARSING_FAILED ?: throw Error.PARSING_FAILED
@ -211,12 +207,12 @@ object OpenGroupAPIV2 {
val headers = mapOf("Authorization" to authToken) val headers = mapOf("Authorization" to authToken)
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token", val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
parameters = parameters, headers = headers, isAuthRequired = false) parameters = parameters, headers = headers, isAuthRequired = false)
return send(request).map(sharedContext) { authToken } return send(request).map { authToken }
} }
fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> { fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> {
val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token") val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token")
return send(request).map(sharedContext) { return send(request).map {
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server) MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
} }
} }
@ -226,14 +222,14 @@ object OpenGroupAPIV2 {
val base64EncodedFile = encodeBytes(file) val base64EncodedFile = encodeBytes(file)
val parameters = mapOf("file" to base64EncodedFile) val parameters = mapOf("file" to base64EncodedFile)
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters) val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
json["result"] as? Long ?: throw Error.PARSING_FAILED json["result"] as? Long ?: throw Error.PARSING_FAILED
} }
} }
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> { fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file") val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
} }
@ -243,7 +239,7 @@ object OpenGroupAPIV2 {
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED) val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
val jsonMessage = signedMessage.toJSON() val jsonMessage = signedMessage.toJSON()
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage) val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any> @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
?: throw Error.PARSING_FAILED ?: throw Error.PARSING_FAILED
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
@ -259,7 +255,7 @@ object OpenGroupAPIV2 {
queryParameters += "from_server_id" to lastId.toString() queryParameters += "from_server_id" to lastId.toString()
} }
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters) val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
return send(request).map(sharedContext) { jsonList -> return send(request).map { jsonList ->
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>> @Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
?: throw Error.PARSING_FAILED ?: throw Error.PARSING_FAILED
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
@ -296,7 +292,7 @@ object OpenGroupAPIV2 {
@JvmStatic @JvmStatic
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> { fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID") val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
return send(request).map(sharedContext) { return send(request).map {
Log.d("Loki", "Deleted server message") Log.d("Loki", "Deleted server message")
} }
} }
@ -308,7 +304,7 @@ object OpenGroupAPIV2 {
queryParameters["from_server_id"] = last.toString() queryParameters["from_server_id"] = last.toString()
} }
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters) val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["ids"]) val idsAsString = JsonUtil.toJson(json["ids"])
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
@ -329,7 +325,7 @@ object OpenGroupAPIV2 {
fun getModerators(room: String, server: String): Promise<List<String>, Exception> { fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators") val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
return send(request).map(sharedContext) { json -> return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String> @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
?: throw Error.PARSING_FAILED ?: throw Error.PARSING_FAILED
val id = "$server.$room" val id = "$server.$room"
@ -342,14 +338,14 @@ object OpenGroupAPIV2 {
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val parameters = mapOf("public_key" to publicKey) val parameters = mapOf("public_key" to publicKey)
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters) val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
return send(request).map(sharedContext) { return send(request).map {
Log.d("Loki", "Banned user $publicKey from $server.$room") Log.d("Loki", "Banned user $publicKey from $server.$room")
} }
} }
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey") val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
return send(request).map(sharedContext) { return send(request).map {
Log.d("Loki", "Unbanned user $publicKey from $server.$room") Log.d("Loki", "Unbanned user $publicKey from $server.$room")
} }
} }
@ -380,7 +376,7 @@ object OpenGroupAPIV2 {
} }
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf("requests" to requests)) val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf("requests" to requests))
// build a request for all rooms // build a request for all rooms
return send(request = request).map(sharedContext) { json -> return send(request = request).map { json ->
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
results.mapNotNull { roomJson -> results.mapNotNull { roomJson ->
@ -462,7 +458,7 @@ object OpenGroupAPIV2 {
fun getInfo(room: String, server: String): Promise<Info, Exception> { fun getInfo(room: String, server: String): Promise<Info, Exception> {
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false) val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
@ -473,7 +469,7 @@ object OpenGroupAPIV2 {
fun getAllRooms(server: String): Promise<List<Info>, Exception> { fun getAllRooms(server: String): Promise<List<Info>, Exception> {
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false) val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
rawRooms.mapNotNull { rawRooms.mapNotNull {
val roomJson = it as? Map<*, *> ?: return@mapNotNull null val roomJson = it as? Map<*, *> ?: return@mapNotNull null
@ -487,7 +483,7 @@ object OpenGroupAPIV2 {
fun getMemberCount(room: String, server: String): Promise<Long, Exception> { fun getMemberCount(room: String, server: String): Promise<Long, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count") val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
return send(request).map(sharedContext) { json -> return send(request).map { json ->
val memberCount = json["member_count"] as? Long ?: throw Error.PARSING_FAILED val memberCount = json["member_count"] as? Long ?: throw Error.PARSING_FAILED
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
storage.setUserCount(room, server, memberCount) storage.setUserCount(room, server, memberCount)

View File

@ -169,15 +169,24 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
Log.d("Loki", "Invalid closed group update.") Log.d("Loki", "Invalid closed group update.")
throw Error.InvalidClosedGroupUpdate 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() } val admins = group.admins.map { it.serialize() }
if (!admins.contains(userPublicKey)) {
Log.d("Loki", "Only an admin can remove members from a group.")
throw Error.InvalidClosedGroupUpdate
}
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
throw Error.InvalidClosedGroupUpdate throw Error.InvalidClosedGroupUpdate
} }
// Save the new group members
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
// Update the zombie list
val oldZombies = storage.getZombieMember(groupID)
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val name = group.title val name = group.title
// Send the update to the group // Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData) val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
@ -185,14 +194,21 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID)) send(closedGroupControlMessage, Address.fromSerialized(groupID))
val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) { // Send the new encryption key pair to the remaining group members
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers) // At this stage we know the user is admin, no need to test
} generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
// Notify the user // Notify the user
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) // Insert an outgoing notification
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, membersToRemove, admins, threadID, sentTime) // we don't display zombie members in the notification as users have already been notified when those members left
val notificationMembers = membersToRemove.minus(oldZombies)
if (notificationMembers.isNotEmpty()) {
// no notification to display when only zombies have been removed
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime)
}
} }
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> { fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {

View File

@ -298,7 +298,8 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val senderPublicKey = message.sender ?: return val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return
val groupPublicKey = kind.publicKey?.toByteArray()?.toHexString() ?: message.groupPublicKey ?: return var groupPublicKey = kind.publicKey?.toByteArray()?.toHexString()
if (groupPublicKey.isNullOrEmpty()) groupPublicKey = message.groupPublicKey ?: return
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val userKeyPair = storage.getUserX25519KeyPair() val userKeyPair = storage.getUserX25519KeyPair()
// Unwrap the message // Unwrap the message
@ -312,7 +313,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
return return
} }
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.") Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
return return
} }
// Find our wrapper and decrypt it if possible // Find our wrapper and decrypt it if possible
@ -393,6 +394,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() } val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Notify the user // Notify the user
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
// sender is a linked device // sender is a linked device
@ -415,6 +417,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
} }
} }
/// Removes the given members from the group IF
/// • it wasn't the admin that was removed (that should happen through a `MEMBER_LEFT` message).
/// • the admin sent the message (only the admin can truly remove members).
/// If we're among the users that were removed, delete all encryption key pairs and the group public key, unsubscribe
/// from push notifications for this closed group, and remove the given members from the zombie list for this group.
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) { private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
@ -428,7 +435,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
return return
} }
if (!group.isActive) { if (!group.isActive) {
Log.d("Loki", "Ignoring closed group info message for inactive group") Log.d("Loki", "Ignoring closed group info message for inactive group.")
return return
} }
val name = group.title val name = group.title
@ -439,6 +446,18 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
// Users that are part of this remove update // Users that are part of this remove update
val updateMembers = kind.members.map { it.toByteArray().toHexString() } val updateMembers = kind.members.map { it.toByteArray().toHexString() }
// Check that the admin wasn't removed
if (updateMembers.contains(admins.first())) {
Log.d("Loki", "Ignoring invalid closed group update.")
return
}
// Check that the message was sent by the group admin
if (!admins.contains(senderPublicKey)) {
Log.d("Loki", "Ignoring invalid closed group update.")
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
// If admin leaves the group is disbanded // If admin leaves the group is disbanded
val didAdminLeave = admins.any { it in updateMembers } val didAdminLeave = admins.any { it in updateMembers }
@ -455,25 +474,34 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
if (didAdminLeave || wasCurrentUserRemoved) { if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else { } else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
} }
// update zombie members
val zombies = storage.getZombieMember(groupID)
storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
val type = if (senderLeft) SignalServiceGroup.Type.QUIT val type = if (senderLeft) SignalServiceGroup.Type.QUIT
else SignalServiceGroup.Type.MEMBER_REMOVED else SignalServiceGroup.Type.MEMBER_REMOVED
// Notify the user // Notify the user
if (userPublicKey == senderPublicKey) { // we don't display zombie members in the notification as users have already been notified when those members left
// sender is a linked device val notificationMembers = updateMembers.minus(zombies)
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) if (notificationMembers.isNotEmpty()) {
storage.insertOutgoingInfoMessage(context, groupID, type, name, updateMembers, admins, threadID, message.sentTimestamp!!) // no notification to display when only zombies have been removed
} else { if (userPublicKey == senderPublicKey) {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, updateMembers, admins, message.sentTimestamp!!) // sender is a linked device
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
} else {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, notificationMembers, admins, message.sentTimestamp!!)
}
} }
} }
/// If a regular member left:
/// • Mark them as a zombie (to be removed by the admin later).
/// If the admin left:
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) { private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
@ -506,11 +534,10 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
// admin left the group of linked device left the group // admin left the group of linked device left the group
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else { } else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) { // update zombie members
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList) val zombies = storage.getZombieMember(groupID)
} storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
} }
// Notify the user // Notify the user
if (userLeft) { if (userLeft) {

View File

@ -7,10 +7,8 @@ import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.successBackground import org.session.libsignal.utilities.successBackground
@ -71,11 +69,8 @@ class ClosedGroupPoller {
// ignore inactive group's messages // ignore inactive group's messages
return@successBackground return@successBackground
} }
messages.forEach { message -> messages.forEach { envelope ->
val rawMessageAsJSON = message as? Map<*, *> val job = MessageReceiveJob(envelope.toByteArray(), false)
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
val data = base64EncodedData?.let { Base64.decode(it) } ?: return@forEach
val job = MessageReceiveJob(MessageWrapper.unwrap(data).toByteArray(), false)
JobQueue.shared.add(job) JobQueue.shared.add(job)
} }
} }

View File

@ -5,11 +5,9 @@ import nl.komponents.kovenant.functional.bind
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeModule import org.session.libsession.snode.SnodeModule
import org.session.libsignal.service.loki.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*

View File

@ -20,9 +20,10 @@ import org.session.libsignal.service.internal.push.ProfileAvatarData
import org.session.libsignal.service.internal.push.PushAttachmentData import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.DigestingRequestBody import org.session.libsignal.service.internal.push.http.DigestingRequestBody
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
import org.session.libsignal.service.loki.api.utilities.HTTP import org.session.libsignal.utilities.Hex
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.service.loki.HTTP
import org.session.libsignal.service.loki.utilities.*
import org.session.libsignal.utilities.* import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log

View File

@ -12,11 +12,13 @@ import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.* import org.session.libsignal.utilities.*
import org.session.libsignal.service.loki.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.api.utilities.* import org.session.libsignal.service.loki.*
import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsession.utilities.getBodyForOnionRequest import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.Broadcaster
import org.session.libsignal.service.loki.HTTP
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.* import org.session.libsignal.service.loki.utilities.*
private typealias Path = List<Snode> private typealias Path = List<Snode>

View File

@ -10,9 +10,9 @@ import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.utilities.getRandomElement import org.session.libsession.snode.utilities.getRandomElement
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.api.utilities.HTTP import org.session.libsignal.service.loki.HTTP
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster import org.session.libsignal.service.loki.Broadcaster
import org.session.libsignal.service.loki.utilities.prettifiedDescription import org.session.libsignal.service.loki.utilities.prettifiedDescription
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.service.loki.utilities.retryIfNeeded

View File

@ -1,7 +1,7 @@
package org.session.libsession.snode package org.session.libsession.snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster import org.session.libsignal.service.loki.Broadcaster
class SnodeModule(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) { class SnodeModule(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {

View File

@ -1,3 +0,0 @@
package org.session.libsession.utilities.mentions
data class Mention(val publicKey: String, val displayName: String)

View File

@ -22,7 +22,6 @@ import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
import org.session.libsignal.service.api.messages.SignalServiceGroup; import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.api.push.SignalServiceAddress; import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.internal.push.PushTransportDetails; import org.session.libsignal.service.internal.push.PushTransportDetails;
import org.session.libsignal.service.internal.push.SignalServiceProtos; import org.session.libsignal.service.internal.push.SignalServiceProtos;
@ -34,7 +33,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMe
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
import org.session.libsignal.service.loki.api.crypto.SessionProtocol; import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities; import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;

View File

@ -1,6 +1,5 @@
package org.session.libsignal.service.api.messages.shared; package org.session.libsignal.service.api.messages.shared;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachment; import org.session.libsignal.service.api.messages.SignalServiceAttachment;

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.utilities package org.session.libsignal.service.loki
interface Broadcaster { interface Broadcaster {

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.api.utilities package org.session.libsignal.service.loki
import okhttp3.* import okhttp3.*
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.database package org.session.libsignal.service.loki
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.Snode import org.session.libsignal.service.loki.Snode

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.database package org.session.libsignal.service.loki
interface LokiMessageDatabaseProtocol { interface LokiMessageDatabaseProtocol {

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.database package org.session.libsignal.service.loki
interface LokiOpenGroupDatabaseProtocol { interface LokiOpenGroupDatabaseProtocol {

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.database package org.session.libsignal.service.loki
interface LokiUserDatabaseProtocol { interface LokiUserDatabaseProtocol {

View File

@ -0,0 +1,3 @@
package org.session.libsignal.service.loki
data class Mention(val publicKey: String, val displayName: String)

View File

@ -1,6 +1,5 @@
package org.session.libsignal.service.loki.crypto package org.session.libsignal.service.loki
import java.io.File
import java.util.zip.CRC32 import java.util.zip.CRC32
/** /**
@ -95,8 +94,10 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
var result = "" var result = ""
val n = truncatedWordSet.size.toLong() val n = truncatedWordSet.size.toLong()
// Check preconditions // Check preconditions
if (words.size < 12) { throw DecodingError.InputTooShort } if (words.size < 12) { throw DecodingError.InputTooShort
if (words.size % 3 == 0) { throw DecodingError.MissingLastWord } }
if (words.size % 3 == 0) { throw DecodingError.MissingLastWord
}
// Get checksum word // Get checksum word
val checksumWord = words.removeAt(words.lastIndex) val checksumWord = words.removeAt(words.lastIndex)
// Decode // Decode
@ -106,7 +107,8 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
val w2 = truncatedWordSet.indexOf(words[chunkStartIndex + 1].substring(0 until prefixLength)) val w2 = truncatedWordSet.indexOf(words[chunkStartIndex + 1].substring(0 until prefixLength))
val w3 = truncatedWordSet.indexOf(words[chunkStartIndex + 2].substring(0 until prefixLength)) val w3 = truncatedWordSet.indexOf(words[chunkStartIndex + 2].substring(0 until prefixLength))
val x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n) val x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n)
if (x % n != w1.toLong()) { throw DecodingError.Generic } if (x % n != w1.toLong()) { throw DecodingError.Generic
}
val string = "0000000" + x.toString(16) val string = "0000000" + x.toString(16)
result += swap(string.substring(string.length - 8 until string.length)) result += swap(string.substring(string.length - 8 until string.length))
} catch (e: Exception) { } catch (e: Exception) {
@ -116,7 +118,8 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
// Verify checksum // Verify checksum
val checksumIndex = determineChecksumIndex(words, prefixLength) val checksumIndex = determineChecksumIndex(words, prefixLength)
val expectedChecksumWord = words[checksumIndex] val expectedChecksumWord = words[checksumIndex]
if (expectedChecksumWord.substring(0 until prefixLength) != checksumWord.substring(0 until prefixLength)) { throw DecodingError.VerificationFailed } if (expectedChecksumWord.substring(0 until prefixLength) != checksumWord.substring(0 until prefixLength)) { throw DecodingError.VerificationFailed
}
// Return // Return
return result return result
} }

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.utilities package org.session.libsignal.service.loki
import org.session.libsignal.service.api.crypto.DigestingOutputStream import org.session.libsignal.service.api.crypto.DigestingOutputStream
import org.session.libsignal.service.internal.push.http.OutputStreamFactory import org.session.libsignal.service.internal.push.http.OutputStreamFactory

View File

@ -1,7 +1,7 @@
package org.session.libsignal.service.loki.api.crypto package org.session.libsignal.service.loki.api.crypto
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
interface SessionProtocol { interface SessionProtocol {

View File

@ -1,4 +1,4 @@
package org.session.libsignal.service.loki.api package org.session.libsignal.service.loki
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos

View File

@ -1,49 +0,0 @@
package org.session.libsignal.service.loki.api.utilities
import okhttp3.MultipartBody
import okhttp3.Request
import okio.Buffer
import org.session.libsignal.utilities.Base64
import java.io.IOException
import java.util.*
internal fun Request.getHeadersForOnionRequest(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
val contentType = body()?.contentType()
if (contentType != null) {
result["content-type"] = contentType.toString()
}
val headers = headers()
for (name in headers.names()) {
val value = headers.get(name)
if (value != null) {
if (value.toLowerCase(Locale.US) == "true" || value.toLowerCase(Locale.US) == "false") {
result[name] = value.toBoolean()
} else if (value.toIntOrNull() != null) {
result[name] = value.toInt()
} else {
result[name] = value
}
}
}
return result
}
internal fun Request.getBodyForOnionRequest(): Any? {
try {
val copyOfThis = newBuilder().build()
val buffer = Buffer()
val body = copyOfThis.body() ?: return null
body.writeTo(buffer)
val bodyAsData = buffer.readByteArray()
if (body is MultipartBody) {
val base64EncodedBody: String = Base64.encodeBytes(bodyAsData)
return mapOf( "fileUpload" to base64EncodedBody )
} else {
val charset = body.contentType()?.charset() ?: Charsets.UTF_8
return bodyAsData?.toString(charset)
}
} catch (e: IOException) {
return null
}
}

View File

@ -64,7 +64,7 @@ public class JsonUtil {
return objectMapper.writeValueAsString(object); return objectMapper.writeValueAsString(object);
} }
public static String toJson(Object object) { public static String toJson(Object object) {
try { try {
return objectMapper.writeValueAsString(object); return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@ -73,32 +73,11 @@ public class JsonUtil {
} }
} }
public static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
}
}
public static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
@Override
public IdentityKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
return new IdentityKey(Base64.decodeWithoutPadding(p.getValueAsString()), 0);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
}
public static ObjectMapper getMapper() { public static ObjectMapper getMapper() {
return objectMapper; return objectMapper;
} }
public static class SaneJSONObject { public static class SaneJSONObject {
private final JSONObject delegate; private final JSONObject delegate;
public SaneJSONObject(JSONObject delegate) { public SaneJSONObject(JSONObject delegate) {

View File

@ -1,25 +1,11 @@
@file:JvmName("PromiseUtilities") @file:JvmName("PromiseUtilities")
package org.session.libsignal.utilities package org.session.libsignal.utilities
import nl.komponents.kovenant.Context
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.jvm.asDispatcher
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.util.concurrent.Executors
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
fun Kovenant.createContext(): Context {
return createContext {
callbackContext.dispatcher = Executors.newSingleThreadExecutor().asDispatcher()
workerContext.dispatcher = ThreadUtils.executorPool.asDispatcher()
multipleCompletion = { v1, v2 ->
Log.d("Loki", "Promise resolved more than once (first with $v1, then with $v2); ignoring $v2.")
}
}
}
fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V { fun <V, E : Throwable> Promise<V, E>.get(defaultValue: V): V {
return try { return try {
get() get()

View File

@ -3,7 +3,6 @@ package org.session.libsignal.utilities
import java.util.concurrent.* import java.util.concurrent.*
object ThreadUtils { object ThreadUtils {
val executorPool = Executors.newCachedThreadPool() val executorPool = Executors.newCachedThreadPool()
@JvmStatic @JvmStatic
@ -17,10 +16,8 @@ object ThreadUtils {
@JvmStatic @JvmStatic
fun newDynamicSingleThreadedExecutor(): ExecutorService { fun newDynamicSingleThreadedExecutor(): ExecutorService {
val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())
LinkedBlockingQueue())
executor.allowCoreThreadTimeOut(true) executor.allowCoreThreadTimeOut(true)
return executor return executor
} }
} }