mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-12 14:13:38 +00:00
implementation of the zombie members handling logic
This commit is contained in:
parent
3e4a6a9df4
commit
b064f8f5d7
@ -28,7 +28,6 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
|||||||
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol;
|
import org.session.libsignal.service.loki.database.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,19 @@ 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) {
|
||||||
|
if (member.isContact()) {
|
||||||
|
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 +314,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 +335,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,17 +353,21 @@ 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));
|
||||||
|
if (serializedMembers != null && !serializedMembers.isEmpty())
|
||||||
return Address.fromSerializedList(serializedMembers, ',');
|
return Address.fromSerializedList(serializedMembers, ',');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,6 +378,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();
|
||||||
|
@ -395,6 +395,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)
|
||||||
}
|
}
|
||||||
@ -403,6 +407,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)
|
||||||
|
@ -54,9 +54,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int lokiV20 = 41;
|
private static final int lokiV20 = 41;
|
||||||
private static final int lokiV21 = 42;
|
private static final int lokiV21 = 42;
|
||||||
private static final int lokiV22 = 43;
|
private static final int lokiV22 = 43;
|
||||||
|
private static final int lokiV23 = 44;
|
||||||
|
|
||||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||||
private static final int DATABASE_VERSION = lokiV22;
|
private static final int DATABASE_VERSION = lokiV23;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -272,6 +273,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
"SendDeliveryReceiptJob");
|
"SendDeliveryReceiptJob");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV23) {
|
||||||
|
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -37,8 +37,13 @@ import java.io.IOException
|
|||||||
|
|
||||||
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private val originalMembers = HashSet<String>()
|
private val originalMembers = HashSet<String>()
|
||||||
private val members = HashSet<String>()
|
|
||||||
private val zombies = HashSet<String>()
|
private val zombies = HashSet<String>()
|
||||||
|
private val members = HashSet<String>()
|
||||||
|
private val allMembers: Set<String>
|
||||||
|
get() {
|
||||||
|
return if (zombies.isNotEmpty()) (members + zombies)
|
||||||
|
else members
|
||||||
|
}
|
||||||
private var hasNameChanged = false
|
private var hasNameChanged = false
|
||||||
private var isSelfAdmin = false
|
private var isSelfAdmin = false
|
||||||
private var isLoading = false
|
private var isLoading = false
|
||||||
@ -124,31 +129,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
|
||||||
|
|
||||||
@ -161,8 +170,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,13 +190,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)
|
|
||||||
|
|
||||||
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
mainContentContainer.visibility = if (allMembers.isEmpty()) View.GONE else View.VISIBLE
|
||||||
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
emptyStateContainer.visibility = if (allMembers.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
@ -204,8 +212,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")
|
||||||
@ -213,7 +222,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)
|
||||||
}
|
}
|
||||||
@ -233,7 +242,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()
|
||||||
@ -241,15 +250,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 {
|
||||||
@ -307,4 +314,6 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GroupMembers(val members: List<String>, val zombieMembers: List<String>) { }
|
||||||
}
|
}
|
@ -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)
|
||||||
|
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
|
||||||
|
return EditClosedGroupActivity.GroupMembers(
|
||||||
|
members.map {
|
||||||
|
it.address.toString()
|
||||||
|
},
|
||||||
|
zombieMembers.map {
|
||||||
it.address.toString()
|
it.address.toString()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,6 +17,7 @@ class EditClosedGroupMembersAdapter(
|
|||||||
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private val members = ArrayList<String>()
|
private val members = ArrayList<String>()
|
||||||
|
private val zombieMembers = ArrayList<String>()
|
||||||
|
|
||||||
fun setMembers(members: Collection<String>) {
|
fun setMembers(members: Collection<String>) {
|
||||||
this.members.clear()
|
this.members.clear()
|
||||||
@ -24,6 +25,12 @@ class EditClosedGroupMembersAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setZombieMembers(members: Collection<String>) {
|
||||||
|
this.zombieMembers.clear()
|
||||||
|
this.zombieMembers.addAll(members)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = members.size
|
override fun getItemCount(): Int = members.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@ -42,6 +49,9 @@ class EditClosedGroupMembersAdapter(
|
|||||||
glide,
|
glide,
|
||||||
if (unlocked) UserView.ActionIndicator.Menu else UserView.ActionIndicator.None)
|
if (unlocked) UserView.ActionIndicator.Menu else UserView.ActionIndicator.None)
|
||||||
|
|
||||||
|
if (zombieMembers.contains(member))
|
||||||
|
viewHolder.view.alpha = 0.5F
|
||||||
|
|
||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) }
|
viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) }
|
||||||
}
|
}
|
||||||
|
@ -109,8 +109,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>
|
||||||
|
@ -355,7 +355,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
|
||||||
@ -458,6 +458,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 = MessagingConfiguration.shared.context
|
val context = MessagingConfiguration.shared.context
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -471,7 +476,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
|
||||||
@ -482,6 +487,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 }
|
||||||
@ -498,12 +515,12 @@ 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
|
||||||
|
|
||||||
@ -517,6 +534,10 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 = MessagingConfiguration.shared.context
|
val context = MessagingConfiguration.shared.context
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -549,11 +570,14 @@ 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)
|
//val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||||
if (isCurrentUserAdmin) {
|
//if (isCurrentUserAdmin) {
|
||||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
// MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||||
}
|
//}
|
||||||
|
// update zombie members
|
||||||
|
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) {
|
||||||
@ -566,7 +590,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
/*val storage = MessagingConfiguration.shared.storage
|
||||||
val senderPublicKey = message.sender ?: return
|
val senderPublicKey = message.sender ?: return
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
if (message.kind!! !is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest) return
|
if (message.kind!! !is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest) return
|
||||||
@ -587,7 +611,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C
|
|||||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||||
} else {
|
} else {
|
||||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
|
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidGroupUpdate(group: GroupRecord,
|
private fun isValidGroupUpdate(group: GroupRecord,
|
||||||
|
@ -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,11 +194,13 @@ 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
|
||||||
|
// At this stage we know the user is admin, no need to test
|
||||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||||
}
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
|
|
||||||
|
// Insert an outgoing notification
|
||||||
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, membersToRemove, admins, threadID, sentTime)
|
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, membersToRemove, admins, threadID, sentTime)
|
||||||
@ -279,6 +290,7 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Note: Shouldn't currently be in use.
|
/// Note: Shouldn't currently be in use.
|
||||||
|
/*
|
||||||
fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
@ -293,7 +305,7 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
|||||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
||||||
closedGroupControlMessage.sentTimestamp = sentTime
|
closedGroupControlMessage.sentTimestamp = sentTime
|
||||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||||
}
|
}*/
|
||||||
|
|
||||||
fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) {
|
fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
|
Loading…
x
Reference in New Issue
Block a user