From 33788189dde6759b6681bff3f121699bc4b641e0 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 12 Dec 2019 10:07:17 +1100 Subject: [PATCH 01/19] Distinguish public chat and rss feed from regular groups --- .../securesms/ApplicationContext.java | 6 +- .../conversation/ConversationActivity.java | 18 +++--- .../securesms/database/Address.java | 28 +++------ .../securesms/database/GroupDatabase.java | 4 ++ .../database/helpers/SQLCipherOpenHelper.java | 41 +++++++++++- .../securesms/groups/GroupManager.java | 57 +++++++++++++---- .../groups/GroupMessageProcessor.java | 14 ++--- .../securesms/jobs/AvatarDownloadJob.java | 19 +++--- .../jobs/MultiDeviceGroupUpdateJob.java | 2 +- .../securesms/jobs/PushDecryptJob.java | 23 ++++--- .../securesms/jobs/PushGroupSendJob.java | 63 +++++++++++-------- .../securesms/jobs/PushGroupUpdateJob.java | 2 +- .../securesms/jobs/RequestGroupInfoJob.java | 2 +- .../securesms/loki/LokiPublicChatManager.kt | 7 +-- .../securesms/loki/LokiPublicChatPoller.kt | 2 +- .../securesms/loki/LokiRSSFeedPoller.kt | 2 +- .../securesms/mms/IncomingMediaMessage.java | 2 +- .../securesms/sms/IncomingTextMessage.java | 2 +- .../securesms/sms/MessageSender.java | 4 +- .../securesms/util/GroupUtil.java | 31 ++++++++- .../securesms/util/IdentityUtil.java | 6 +- 21 files changed, 220 insertions(+), 115 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 3da175eeb7..12d605620b 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -529,7 +529,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc public void createDefaultPublicChatsIfNeeded() { List defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); for (LokiPublicChat publiChat : defaultPublicChats) { - long threadID = GroupManager.getThreadId(publiChat.getId(), this); + long threadID = GroupManager.getPublicChatThreadId(publiChat.getId(), this); String migrationKey = publiChat.getId() + "_migrated"; boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publiChat.getId()); @@ -552,7 +552,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc for (LokiRSSFeed feed : feeds) { boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId()); if (!isFeedSetUp || !feed.isDeletable()) { - GroupManager.createGroup(feed.getId(), this, new HashSet<>(), null, feed.getDisplayName(), false); + GroupManager.createRSSFeedGroup(feed.getId(), this, null, feed.getDisplayName()); TextSecurePreferences.markChatSetUp(this, feed.getId()); } } @@ -561,7 +561,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc private void createRSSFeedPollersIfNeeded() { // Only create the RSS feed pollers if their threads aren't deleted LokiRSSFeed lokiNewsFeed = lokiNewsFeed(); - long lokiNewsFeedThreadID = GroupManager.getThreadId(lokiNewsFeed.getId(), this); + long lokiNewsFeedThreadID = GroupManager.getRSSFeedThreadId(lokiNewsFeed.getId(), this); if (lokiNewsFeedThreadID >= 0 && lokiNewsFeedPoller == null) { lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed); // Set up deletion listeners if needed diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 16641984dd..f40d5aff44 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -442,10 +442,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, this); if (this.recipient.isGroupRecipient()) { - if (this.recipient.getName().equals("Loki Public Chat")) { - Analytics.Companion.getShared().track("Loki Public Chat Opened"); - } else { + if (this.recipient.getAddress().isPublicChat()) { + Analytics.Companion.getShared().track("Public Chat Opened"); + } else if (this.recipient.getAddress().isRSSFeed()) { Analytics.Companion.getShared().track("RSS Feed Opened"); + } else { + Analytics.Companion.getShared().track("Private Group Chat Opened"); } } else { Analytics.Companion.getShared().track("Conversation Opened"); @@ -687,9 +689,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity MenuInflater inflater = this.getMenuInflater(); menu.clear(); - boolean isLokiPublicChat = isGroupConversation(); // TODO: Figure out a better way of determining this + boolean isLokiGroupChat = recipient.getAddress().isPublicChat() || recipient.getAddress().isRSSFeed(); - if (isSecureText && !isLokiPublicChat) { // TODO: + if (isSecureText && !isLokiGroupChat) { if (recipient.getExpireMessages() > 0) { inflater.inflate(R.menu.conversation_expiring_on, menu); @@ -709,7 +711,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu); else inflater.inflate(R.menu.conversation_callable_insecure, menu); */ - } else if (isGroupConversation() && !isLokiPublicChat) { + } else if (isGroupConversation() && !isLokiGroupChat) { inflater.inflate(R.menu.conversation_group_options, menu); if (!isPushGroupConversation()) { @@ -2007,7 +2009,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) { - if (recipient.isGroupRecipient() && (recipient.getName().equals("Loki News") || recipient.getName().equals("Loki Messenger Updates"))) { + if (recipient.isGroupRecipient() && recipient.getAddress().isRSSFeed()) { unblockButton.setVisibility(View.GONE); composePanel.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); @@ -2036,7 +2038,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void setGroupShareProfileReminder(@NonNull Recipient recipient) { - if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing()) { + if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing() && !recipient.getAddress().isPublicChat() && !recipient.getAddress().isRSSFeed()) { groupShareProfileView.get().setRecipient(recipient); groupShareProfileView.get().setVisibility(View.VISIBLE); } else if (groupShareProfileView.resolved()) { diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java index d063025cfa..1286b4b302 100644 --- a/src/org/thoughtcrime/securesms/database/Address.java +++ b/src/org/thoughtcrime/securesms/database/Address.java @@ -52,17 +52,9 @@ public class Address implements Parcelable, Comparable
{ private final String address; - // Loki - Special flag to indicate whether this address represents a public chat or not - private Boolean isPublicChat; - private Address(@NonNull String address) { - this(address, false); - } - - private Address(@NonNull String address, Boolean isPublicChat) { if (address == null) throw new AssertionError(address); this.address = address.toLowerCase(); - this.isPublicChat = isPublicChat; } public Address(Parcel in) { @@ -77,10 +69,6 @@ public class Address implements Parcelable, Comparable
{ return Address.fromSerialized(external); } - public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) { - return new Address(serialized, true); - } - public static @NonNull List
fromSerializedList(@NonNull String serialized, char delimiter) { String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter); List
addresses = new LinkedList<>(); @@ -121,13 +109,15 @@ public class Address implements Parcelable, Comparable
{ } } - public boolean isGroup() { - return GroupUtil.isEncodedGroup(address); - } + public boolean isGroup() { return GroupUtil.isEncodedGroup(address); } - public boolean isMmsGroup() { - return GroupUtil.isMmsGroup(address); - } + public boolean isSignalGroup() { return !isPublicChat() && !isRSSFeed(); } + + public boolean isPublicChat() { return GroupUtil.isPublicChat(address); } + + public boolean isRSSFeed() { return GroupUtil.isRssFeed(address); } + + public boolean isMmsGroup() { return GroupUtil.isMmsGroup(address); } public boolean isEmail() { return NumberUtil.isValidEmail(address); @@ -143,7 +133,7 @@ public class Address implements Parcelable, Comparable
{ } public @NonNull String toPhoneString() { - if (!isPhone() && !isPublicChat) { + if (!isPhone() && !isPublicChat()) { if (isEmail()) throw new AssertionError("Not e164, is email"); if (isGroup()) throw new AssertionError("Not e164, is group"); throw new AssertionError("Not e164, unknown"); diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 747ed36e41..9d2a815cb7 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -448,6 +448,10 @@ public class GroupDatabase extends Database { return mms; } + public boolean isPublicChat() { return Address.fromSerialized(id).isPublicChat(); } + + public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); } + public String getUrl() { return url; } } } diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 6715feb5ee..d0b12cdd42 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -37,10 +37,14 @@ import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.*; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; import java.io.File; +import java.security.acl.Group; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -71,8 +75,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV2 = 23; private static final int lokiV3 = 24; private static final int lokiV4 = 25; + private static final int lokiV5 = 26; - private static final int DATABASE_VERSION = lokiV4; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV5; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -510,6 +515,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand()); } + if (oldVersion < lokiV5) { + // Migrate public chats from __textsecure_group__ to __loki_public_chat_group__ + try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) { + while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) { + String chatString = lokiPublicChatCursor.getString(0); + LokiPublicChat publicChat = LokiPublicChat.fromJSON(chatString); + if (publicChat != null) { + byte[] groupId = publicChat.getId().getBytes(); + String oldId = GroupUtil.getEncodedId(groupId, false); + String newId = GroupUtil.getEncodedPublicChatId(groupId); + ContentValues threadUpdate = new ContentValues(); + threadUpdate.put("recipient_ids", newId); + db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId }); + ContentValues groupUpdate = new ContentValues(); + groupUpdate.put("group_id", newId); + db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId }); + } + } + } + + // Migrate rss feeds from __textsecure_group__ to __loki_rss_feed_group__ + String[] rssFeedIds = new String[] { "loki.network.feed", "loki.network.messenger-updates.feed" }; + for (String groupId : rssFeedIds) { + String oldId = GroupUtil.getEncodedId(groupId.getBytes(), false); + String newId = GroupUtil.getEncodedRSSFeedId(groupId.getBytes()); + ContentValues threadUpdate = new ContentValues(); + threadUpdate.put("recipient_ids", newId); + db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId }); + ContentValues groupUpdate = new ContentValues(); + groupUpdate.put("group_id", newId); + db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId }); + } + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index e17b964d93..8f5004fbe3 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -36,8 +36,13 @@ import java.util.Set; public class GroupManager { - public static long getThreadId(String id, @NonNull Context context) { - final String groupId = GroupUtil.getEncodedId(id.getBytes(), false); + public static long getPublicChatThreadId(String id, @NonNull Context context) { + final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes()); + return getThreadIdFromGroupId(groupId, context); + } + + public static long getRSSFeedThreadId(String id, @NonNull Context context) { + final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes()); return getThreadIdFromGroupId(groupId, context); } @@ -73,16 +78,6 @@ public class GroupManager { memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null); - if (!mms) { - groupDatabase.updateAvatar(groupId, avatarBytes); - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); - } - - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); - return new GroupActionResult(groupRecipient, threadId); - - /* Loki: Original Code - ================== if (!mms) { groupDatabase.updateAvatar(groupId, avatarBytes); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); @@ -91,7 +86,43 @@ public class GroupManager { long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); } - */ + } + + public static @NonNull GroupActionResult createPublicChatGroup(@NonNull String id, + @NonNull Context context, + @Nullable Bitmap avatar, + @Nullable String name) + { + final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes()); + return createLokiGroup(groupId, context, avatar, name); + } + + public static @NonNull GroupActionResult createRSSFeedGroup(@NonNull String id, + @NonNull Context context, + @Nullable Bitmap avatar, + @Nullable String name) + { + final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes()); + return createLokiGroup(groupId, context, avatar, name); + } + + private static @NonNull GroupActionResult createLokiGroup(@NonNull String groupId, + @NonNull Context context, + @Nullable Bitmap avatar, + @Nullable String name) + { + final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); + final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); + final Set
memberAddresses = new HashSet<>(); + + memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); + groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null); + + groupDatabase.updateAvatar(groupId, avatarBytes); + + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); + return new GroupActionResult(groupRecipient, threadId); } public static GroupActionResult updateGroup(@NonNull Context context, diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 4033a8823f..49df8e0e37 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -58,7 +58,7 @@ public class GroupMessageProcessor { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); SignalServiceGroup group = message.getGroupInfo().get(); - String id = GroupUtil.getEncodedId(group.getGroupId(), false); + String id = GroupUtil.getEncodedId(group); Optional record = database.getGroup(id); if (record.isPresent() && group.getType() == Type.UPDATE) { @@ -81,7 +81,7 @@ public class GroupMessageProcessor { boolean outgoing) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - String id = GroupUtil.getEncodedId(group.getGroupId(), false); + String id = GroupUtil.getEncodedId(group); GroupContext.Builder builder = createGroupContext(group); builder.setType(GroupContext.Type.UPDATE); @@ -108,7 +108,7 @@ public class GroupMessageProcessor { { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - String id = GroupUtil.getEncodedId(group.getGroupId(), false); + String id = GroupUtil.getEncodedId(group); Set
recordMembers = new HashSet<>(groupRecord.getMembers()); Set
messageMembers = new HashSet<>(); @@ -179,7 +179,7 @@ public class GroupMessageProcessor { boolean outgoing) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - String id = GroupUtil.getEncodedId(group.getGroupId(), false); + String id = GroupUtil.getEncodedId(group); List
members = record.getMembers(); GroupContext.Builder builder = createGroupContext(group); @@ -204,14 +204,14 @@ public class GroupMessageProcessor { { if (group.getAvatar().isPresent()) { ApplicationContext.getInstance(context).getJobManager() - .add(new AvatarDownloadJob(group.getGroupId())); + .add(new AvatarDownloadJob(GroupUtil.getEncodedId(group))); } try { if (outgoing) { MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); - Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false)); - Recipient recipient = Recipient.from(context, addres, false); + Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group)); + Recipient recipient = Recipient.from(context, address, false); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 3536051086..77e4e1c883 100644 --- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -40,9 +40,9 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { @Inject SignalServiceMessageReceiver receiver; - private byte[] groupId; + private String groupId; - public AvatarDownloadJob(@NonNull byte[] groupId) { + public AvatarDownloadJob(@NonNull String groupId) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(10) @@ -50,14 +50,14 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { groupId); } - private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) { + private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) { super(parameters); this.groupId = groupId; } @Override public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build(); + return new Data.Builder().putString(KEY_GROUP_ID, groupId).build(); } @Override @@ -67,9 +67,8 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { @Override public void onRun() throws IOException { - String encodeId = GroupUtil.getEncodedId(groupId, false); GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - Optional record = database.getGroup(encodeId); + Optional record = database.getGroup(groupId); File attachment = null; try { @@ -97,7 +96,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); - database.updateAvatar(encodeId, avatar); + database.updateAvatar(groupId, avatar); inputStream.close(); } } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { @@ -120,11 +119,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { public static final class Factory implements Job.Factory { @Override public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { - try { - return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID))); - } catch (IOException e) { - throw new AssertionError(e); - } + return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID)); } } } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java index be44c92233..52a81d5d57 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java @@ -85,7 +85,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob implements InjectableType reader = DatabaseFactory.getGroupDatabase(context).getGroups(); while ((record = reader.getNext()) != null) { - if (!record.isMms()) { + if (!record.isMms() && !record.isPublicChat() && !record.isRSSFeed()) { List members = new LinkedList<>(); for (Address member : record.getMembers()) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 641710a6a0..99e05c6565 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -350,7 +350,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, Optional.absent()); - if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) { + if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) { handleUnknownGroupMessage(content, message.getGroupInfo().get()); } @@ -606,9 +606,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceGroup group) { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId())); + if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId())); + } } private void handleExpirationUpdate(@NonNull SignalServiceContent content, @@ -733,7 +735,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { threadId = handleSynchronizeSentTextMessage(message); } - if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false))) { + if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) { handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); } @@ -743,7 +745,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Recipient recipient = null; if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false); - else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false); + else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false); if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { @@ -1571,10 +1573,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { long threadId; if (typingMessage.getGroupId().isPresent()) { + // Typing messages should only apply to signal groups, thus we use `getEncodedId` Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false)); Recipient groupRecipient = Recipient.from(context, groupAddress, false); - threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(groupRecipient); } else { // See if we need to redirect the message author = getPrimaryDeviceRecipient(content.getSender()); @@ -1723,7 +1726,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private Recipient getSyncMessageDestination(SentTranscriptMessage message) { if (message.getMessage().getGroupInfo().isPresent()) { - return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false); + return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false); } else { return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false); } @@ -1739,7 +1742,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { if (message.getGroupInfo().isPresent()) { - return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); + return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false); } else { return Recipient.from(context, Address.fromSerialized(content.getSender()), false); } @@ -1804,7 +1807,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { return true; } else if (conversation.isGroupRecipient()) { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)) + Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get())) : Optional.absent(); if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index d5d4fa6c99..37b4c3ad8f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; @@ -231,7 +232,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { throws IOException, UntrustedIdentityException, UndeliverableMessageException { // rotateSenderCertificateIfNecessary(); - String groupId = message.getRecipient().getAddress().toGroupString(); + // Messages shouldn't be able to be sent to RSS Feeds + Address groupAddress = message.getRecipient().getAddress(); + if (groupAddress.isRSSFeed()) { + List results = new ArrayList<>(); + for (Address destination : destinations) results.add(SendMessageResult.networkFailure(new SignalServiceAddress(destination.toPhoneString()))); + return results; + } + + String groupId = groupAddress.toGroupString(); Optional profileKey = getProfileKey(message.getRecipient()); Optional quote = getQuoteFor(message); Optional sticker = getStickerFor(message); @@ -247,24 +256,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) .toList(); - if (message.isGroup()) { + SignalServiceGroup.GroupType groupType = SignalServiceGroup.GroupType.SIGNAL; + if (groupAddress.isPublicChat()) { + groupType = SignalServiceGroup.GroupType.PUBLIC_CHAT; + } + + if (message.isGroup() && groupAddress.isSignalGroup()) { + // Loki - Only send GroupUpdate or GroupQuit to signal groups OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; GroupContext groupContext = groupMessage.getGroupContext(); SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; - SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), groupContext.getMembersList(), avatar); + SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar); SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .withExpiration(message.getRecipient().getExpireMessages()) + .withBody(message.getBody()) .asGroupMessage(group) .build(); - // Loki - Disable group updates for now - List results = new ArrayList<>(); - for (Address destination : destinations) results.add(SendMessageResult.success(new SignalServiceAddress(destination.toPhoneString()), false, false)); - return results; + return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage); } else { - SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId)); + SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId), groupType); SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .asGroupMessage(group) @@ -284,26 +297,24 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } private @NonNull List
getGroupMessageRecipients(String groupId, long messageId) { - ArrayList
result = new ArrayList<>(); + if (GroupUtil.isRssFeed(groupId)) { return new ArrayList<>(); } - // Loki - All group messages should be directed to their respective servers - long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); - LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); - if (publicChat != null) { - // We need to somehow maintain information that will allow the sender to map - // a recipient to the correct public chat thread, and so this might be a bit hacky - result.add(Address.fromPublicChatGroupID(groupId)); + // Loki - All public chat group messages should be directed to their respective servers + if (GroupUtil.isPublicChat(groupId)) { + ArrayList
result = new ArrayList<>(); + long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); + if (publicChat != null) { + result.add(Address.fromSerialized(groupId)); + } + return result; + } else { + List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); + if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList(); + + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); + return Stream.of(members).map(Recipient::getAddress).toList(); } - - return result; - - /* - List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); - if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList(); - - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); - return Stream.of(members).map(Recipient::getAddress).toList(); - */ } public static class Factory implements Job.Factory { diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index e12d094c11..d35dd2925f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -104,7 +104,7 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType { SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE) .withAvatar(avatar) - .withId(groupId) + .withId(groupId, SignalServiceGroup.GroupType.SIGNAL) .withMembers(members) .withName(record.get().getTitle()) .build(); diff --git a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java index 380aeadc9b..795e1060c0 100644 --- a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java @@ -71,7 +71,7 @@ public class RequestGroupInfoJob extends BaseJob implements InjectableType { @Override public void onRun() throws IOException, UntrustedIdentityException { SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO) - .withId(groupId) + .withId(groupId, SignalServiceGroup.GroupType.SIGNAL) .build(); SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt index 01da3532ad..a4197ba156 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.LokiPublicChat -import java.util.* class LokiPublicChatManager(private val context: Context) { private var chats = mutableMapOf() @@ -49,10 +48,10 @@ class LokiPublicChatManager(private val context: Context) { public fun addChat(server: String, channel: Long, name: String): LokiPublicChat { val chat = LokiPublicChat(channel, server, name, true) - var threadID = GroupManager.getThreadId(chat.id, context) + var threadID = GroupManager.getPublicChatThreadId(chat.id, context) // Create the group if we don't have one if (threadID < 0) { - val result = GroupManager.createGroup(chat.id, context, HashSet(), null, chat.displayName, false) + val result = GroupManager.createPublicChatGroup(chat.id, context, null, chat.displayName) threadID = result.threadId } DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID) @@ -73,7 +72,7 @@ class LokiPublicChatManager(private val context: Context) { removedChatThreadIds.forEach { pollers.remove(it)?.stop() } // Only append to chats if we have a thread for the chat - chats = chatsInDB.filter { GroupManager.getThreadId(it.value.id, context) > -1 }.toMutableMap() + chats = chatsInDB.filter { GroupManager.getPublicChatThreadId(it.value.id, context) > -1 }.toMutableMap() } private fun listenToThreadDeletion(threadID: Long) { diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt index b1af5cc08c..2667838ad8 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt @@ -112,7 +112,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki // region Polling private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage { val id = group.id.toByteArray() - val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) + val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null) val quote = if (message.quote != null) { SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf()) } else { diff --git a/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt index d695c7de26..30d3b7c269 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt @@ -64,7 +64,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF bodyAsHTML = matcher.replaceAll("$2 ($1)") val body = Html.fromHtml(bodyAsHTML).toString().trim() val id = feed.id.toByteArray() - val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) + val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null) val x2 = SignalServiceDataMessage(timestamp, x1, null, body) val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false) PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent()) diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index 8d0c19962c..f4d0a4889e 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -79,7 +79,7 @@ public class IncomingMediaMessage { this.quote = quote.orNull(); this.unidentified = unidentified; - if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false)); + if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get())); else this.groupId = null; this.attachments.addAll(PointerAttachment.forPointers(attachments)); diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index cfafcb7132..1c787355c3 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -78,7 +78,7 @@ public class IncomingTextMessage implements Parcelable { this.unidentified = unidentified; if (group.isPresent()) { - this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false)); + this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get())); } else { this.groupId = null; } diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 9bb30f7dc6..744a81c39a 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -202,7 +202,7 @@ public class MessageSender { if (attachment != null) { message.getAttachments().add(attachment); } long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); // Loki - Set the message's friend request status as soon as it has hit the database - if (message.isFriendRequest) { + if (message.isFriendRequest && !recipient.getAddress().isGroup()) { FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId); } sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); @@ -215,7 +215,7 @@ public class MessageSender { try { long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); // Loki - Set the message's friend request status as soon as it has hit the database - if (message.isFriendRequest) { + if (message.isFriendRequest && !recipient.getAddress().isGroup()) { FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId); } sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 224887fb0a..b8d2500a32 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import java.io.IOException; import java.util.Collections; @@ -28,12 +29,32 @@ public class GroupUtil { private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!"; private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!"; + private static final String ENCODED_PUBLIC_CHAT_GROUP_PREFIX = "__loki_public_chat_group__!"; + private static final String ENCODED_RSS_FEED_GROUP_PREFIX = "__loki_rss_feed_group__!"; private static final String TAG = GroupUtil.class.getSimpleName(); + public static String getEncodedId(SignalServiceGroup group) { + byte[] groupId = group.getGroupId(); + if (group.getGroupType() == SignalServiceGroup.GroupType.PUBLIC_CHAT) { + return getEncodedPublicChatId(groupId); + } else if (group.getGroupType() == SignalServiceGroup.GroupType.RSS_FEED) { + return getEncodedRSSFeedId(groupId); + } + return getEncodedId(groupId, false); + } + public static String getEncodedId(byte[] groupId, boolean mms) { return (mms ? ENCODED_MMS_GROUP_PREFIX : ENCODED_SIGNAL_GROUP_PREFIX) + Hex.toStringCondensed(groupId); } + public static String getEncodedPublicChatId(byte[] groupId) { + return ENCODED_PUBLIC_CHAT_GROUP_PREFIX + Hex.toStringCondensed(groupId); + } + + public static String getEncodedRSSFeedId(byte[] groupId) { + return ENCODED_RSS_FEED_GROUP_PREFIX + Hex.toStringCondensed(groupId); + } + public static byte[] getDecodedId(String groupId) throws IOException { if (!isEncodedGroup(groupId)) { throw new IOException("Invalid encoding"); @@ -48,13 +69,21 @@ public class GroupUtil { } public static boolean isEncodedGroup(@NonNull String groupId) { - return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); + return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX) || groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX) || groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX); } public static boolean isMmsGroup(@NonNull String groupId) { return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); } + public static boolean isPublicChat(@NonNull String groupId) { + return groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX); + } + + public static boolean isRssFeed(@NonNull String groupId) { + return groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX); + } + @WorkerThread public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { String encodedGroupId = groupRecipient.getAddress().toGroupString(); diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java index 07336f0629..56dff28e6a 100644 --- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -74,8 +74,9 @@ public class IdentityUtil { GroupDatabase.GroupRecord groupRecord; while ((groupRecord = reader.getNext()) != null) { + if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; } if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive() && !groupRecord.isMms()) { - SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId()); + SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL); if (remote) { IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false); @@ -126,8 +127,9 @@ public class IdentityUtil { GroupDatabase.GroupRecord groupRecord; while ((groupRecord = reader.getNext()) != null) { + if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; } if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) { - SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId()); + SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL); IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false); IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming); From 23a5fa7580a499896aa1fcd12a8bb6f5c46d2b33 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 12 Dec 2019 16:07:13 +1100 Subject: [PATCH 02/19] Let user know when members have been removed from the group. --- res/values/strings.xml | 5 ++ .../conversation/ConversationUpdateItem.java | 2 +- .../database/model/MessageRecord.java | 2 +- .../groups/GroupMessageProcessor.java | 4 +- .../securesms/loki/LokiThreadDatabase.kt | 4 + .../securesms/util/GroupUtil.java | 75 +++++++++++++++---- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index a6cd89e760..73605e508f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1073,7 +1073,12 @@ %1$s joined the group. %1$s joined the group. + + %1$s was removed from the group. + %1$s were removed from the group. + Group name is now \'%1$s\'. + You were removed from the group. Make your profile name and photo visible to this group? diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 2d8c98bee0..5e3e103d23 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -175,7 +175,7 @@ public class ConversationUpdateItem extends LinearLayout icon.setImageResource(R.drawable.ic_group_grey600_24dp); icon.clearColorFilter(); - GroupUtil.getDescription(getContext(), messageRecord.getBody()).addListener(this); + GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.getRecipient()).addListener(this); body.setText(messageRecord.getDisplayBody(getContext())); title.setVisibility(GONE); diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 0531562b73..a2b324c7cb 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -92,7 +92,7 @@ public abstract class MessageRecord extends DisplayRecord { if (isGroupUpdate() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); } else if (isGroupUpdate()) { - return new SpannableString(GroupUtil.getDescription(context, getBody()).toString(getIndividualRecipient())); + return new SpannableString(GroupUtil.getDescription(context, getBody(), getRecipient()).toString(getIndividualRecipient())); } else if (isGroupQuit() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_left_group)); } else if (isGroupQuit()) { diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 49df8e0e37..f184231a72 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -141,7 +141,9 @@ public class GroupMessageProcessor { } if (missingMembers.size() > 0) { - // TODO We should tell added and missing about each-other. + for (Address removedMember : missingMembers) { + builder.addMembers(removedMember.serialize()); + } } if (group.getName().isPresent() || group.getAvatar().isPresent()) { diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index dfc732474c..ac5bde0b08 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -45,6 +45,10 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus { if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE } + // Loki - Friend request logic doesn't apply to group chats, always treat them as friends + val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID) + if (recipient != null && recipient.isGroupRecipient) { return LokiThreadFriendRequestStatus.FRIENDS; } + val database = databaseHelper.readableDatabase val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> cursor.getInt(friendRequestStatus) diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index b8d2500a32..3dc6c363a7 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -7,10 +7,13 @@ import android.support.annotation.WorkerThread; import com.google.protobuf.ByteString; +import network.loki.messenger.BuildConfig; import network.loki.messenger.R; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase.*; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; @@ -111,17 +114,25 @@ public class GroupUtil { } - public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) { + public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, @Nullable Recipient groupRecipient) { + // Make sure we always are passing a group recipient + if (BuildConfig.DEBUG && groupRecipient != null && !groupRecipient.isGroupRecipient()) { + throw new AssertionError(); + } + if (encodedGroup == null) { - return new GroupDescription(context, null); + return new GroupDescription(context, null, null); } try { - GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); - return new GroupDescription(context, groupContext); + GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); + GroupRecord groupRecord = groupRecipient != null + ? DatabaseFactory.getGroupDatabase(context).getGroup(groupRecipient.getAddress().toGroupString()).orNull() + : null; + return new GroupDescription(context, groupContext, groupRecord); } catch (IOException e) { Log.w(TAG, e); - return new GroupDescription(context, null); + return new GroupDescription(context, null, null); } } @@ -129,24 +140,50 @@ public class GroupUtil { @NonNull private final Context context; @Nullable private final GroupContext groupContext; - @Nullable private final List members; + private final List members; + private final List removedMembers; + private boolean ourDeviceWasRemoved; - public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { + public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { this(context, groupContext, null); } + public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext, @Nullable GroupRecord groupRecord) { this.context = context.getApplicationContext(); this.groupContext = groupContext; - if (groupContext == null || groupContext.getMembersList().isEmpty()) { - this.members = null; - } else { - this.members = new LinkedList<>(); + this.members = new LinkedList<>(); + this.removedMembers = new LinkedList<>(); + this.ourDeviceWasRemoved = false; - for (String member : groupContext.getMembersList()) { - this.members.add(Recipient.from(context, Address.fromExternal(context, member), true)); + if (groupContext != null && !groupContext.getMembersList().isEmpty()) { + List memberList = groupContext.getMembersList(); + List
currentMembers = groupRecord != null ? groupRecord.getMembers() : null; + + // Add them to the member or removed members lists + for (String member : memberList) { + Address address = Address.fromSerialized(member); + Recipient recipient = Recipient.from(context, address, true); + if (currentMembers == null || currentMembers.contains(address)) { + this.members.add(recipient); + } else { + this.removedMembers.add(recipient); + } + } + + // Check if our device was removed + if (!removedMembers.isEmpty()) { + String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); + String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); + Recipient self = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false); + ourDeviceWasRemoved = removedMembers.contains(self); } } } public String toString(Recipient sender) { + // Show the local removed message + if (ourDeviceWasRemoved) { + return context.getString(R.string.GroupUtil_you_were_removed_from_group); + } + StringBuilder description = new StringBuilder(); description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString())); @@ -156,14 +193,20 @@ public class GroupUtil { String title = groupContext.getName(); - if (members != null) { + if (!members.isEmpty()) { description.append("\n"); description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group, members.size(), toString(members))); } + if (!removedMembers.isEmpty()) { + description.append("\n"); + description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_removed_from_the_group, + removedMembers.size(), toString(removedMembers))); + } + if (title != null && !title.trim().isEmpty()) { - if (members != null) description.append(" "); + if (!members.isEmpty()) description.append(" "); else description.append("\n"); description.append(context.getString(R.string.GroupUtil_group_name_is_now, title)); } @@ -172,7 +215,7 @@ public class GroupUtil { } public void addListener(RecipientModifiedListener listener) { - if (this.members != null) { + if (!this.members.isEmpty()) { for (Recipient member : this.members) { member.addListener(listener); } From b6d2717286bf7c759e25e6170716c39a53c1ff05 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 13 Dec 2019 10:25:53 +1100 Subject: [PATCH 03/19] Added admins to groups. Only process group updates if an admin sent it. --- .../securesms/GroupCreateActivity.java | 40 +++++++++++++------ .../securesms/database/GroupDatabase.java | 38 ++++++++++++++---- .../securesms/database/SmsMigrator.java | 2 +- .../database/helpers/SQLCipherOpenHelper.java | 12 ++++-- .../securesms/groups/GroupManager.java | 34 +++++++++++----- .../groups/GroupMessageProcessor.java | 39 +++++++++++++++++- .../securesms/jobs/MmsDownloadJob.java | 2 +- .../securesms/jobs/PushGroupSendJob.java | 2 +- .../securesms/jobs/PushGroupUpdateJob.java | 7 +++- .../securesms/loki/LokiPublicChatPoller.kt | 3 +- .../securesms/loki/LokiRSSFeedPoller.kt | 2 +- 11 files changed, 139 insertions(+), 42 deletions(-) diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 9408b45bd2..b35d8f77b9 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -72,6 +72,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -246,7 +247,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity return; } if (isSignalGroup()) { - new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + Recipient local = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false); + new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients(), Collections.singleton(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -254,7 +256,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private void handleGroupUpdate() { new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp, - getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + getGroupName(), getAdapter().getRecipients(), groupToUpdate.get().admins).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void handleOpenConversation(long threadId, Recipient recipient) { @@ -344,9 +346,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity for (Recipient recipient : members) { memberAddresses.add(recipient.getAddress()); } - memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity))); + Address local = Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity)); + memberAddresses.add(local); - String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true); + String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true, Collections.singletonList(local)); Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true); long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); @@ -370,16 +373,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity protected Bitmap avatar; protected Set members; protected String name; + protected Set admins; public SignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, - Set members) + Set members, + Set admins) { this.activity = activity; this.avatar = avatar; this.name = name; this.members = members; + this.admins = admins; } @Override @@ -403,13 +409,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity } private static class CreateSignalGroupTask extends SignalGroupTask { - public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set members) { - super(activity, avatar, name, members); + public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set members, Set admins) { + super(activity, avatar, name, members, admins); } @Override protected Optional doInBackground(Void... aVoid) { - return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false)); + return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins)); } @Override @@ -430,16 +436,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private String groupId; public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId, - Bitmap avatar, String name, Set members) + Bitmap avatar, String name, Set members, Set admins) { - super(activity, avatar, name, members); + super(activity, avatar, name, members, admins); this.groupId = groupId; } @Override protected Optional doInBackground(Void... aVoid) { try { - return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name)); + return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name, admins)); } catch (InvalidNumberException e) { return Optional.absent(); } @@ -537,11 +543,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity existingContacts.addAll(recipients); if (group.isPresent()) { + List
adminList = group.get().getAdmins(); + final Set admins = new HashSet<>(adminList.size()); + for (Address admin : adminList) { + admins.add(Recipient.from(getContext(), admin, false)); + } return Optional.of(new GroupData(groupIds[0], existingContacts, BitmapUtil.fromByteArray(group.get().getAvatar()), group.get().getAvatar(), - group.get().getTitle())); + group.get().getTitle(), + admins)); } else { return Optional.absent(); } @@ -582,13 +594,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity Bitmap avatarBmp; byte[] avatarBytes; String name; + Set admins; - public GroupData(String id, Set recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) { + public GroupData(String id, Set recipients, Bitmap avatarBmp, byte[] avatarBytes, String name, Set admins) { this.id = id; this.recipients = recipients; this.avatarBmp = avatarBmp; this.avatarBytes = avatarBytes; this.name = name; + this.admins = admins; } } } diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 9d2a815cb7..19108e703e 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -24,7 +24,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin import java.io.Closeable; import java.io.IOException; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Collections; import java.util.LinkedList; @@ -52,6 +51,7 @@ public class GroupDatabase extends Database { // Loki private static final String AVATAR_URL = "avatar_url"; + private static final String ADMINS = "admins"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + @@ -68,6 +68,7 @@ public class GroupDatabase extends Database { ACTIVE + " INTEGER DEFAULT 1, " + AVATAR_DIGEST + " BLOB, " + AVATAR_URL + " TEXT, " + + ADMINS + "TEXT, " + MMS + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { @@ -76,7 +77,7 @@ public class GroupDatabase extends Database { private static final String[] GROUP_PROJECTION = { GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, - TIMESTAMP, ACTIVE, MMS, AVATAR_URL + TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS }; static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); @@ -116,8 +117,9 @@ public class GroupDatabase extends Database { return new Reader(cursor); } - public String getOrCreateGroupForMembers(List
members, boolean mms) { + public String getOrCreateGroupForMembers(List
members, boolean mms, List
admins) { Collections.sort(members); + Collections.sort(admins); Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID}, MEMBERS + " = ? AND " + MMS + " = ?", @@ -128,7 +130,7 @@ public class GroupDatabase extends Database { return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)); } else { String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms); - create(groupId, null, members, null, null); + create(groupId, null, members, null, null, admins); return groupId; } } finally { @@ -157,7 +159,7 @@ public class GroupDatabase extends Database { } public void create(@NonNull String groupId, @Nullable String title, @NonNull List
members, - @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay) + @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins) { Collections.sort(members); @@ -179,6 +181,10 @@ public class GroupDatabase extends Database { contentValues.put(ACTIVE, 1); contentValues.put(MMS, GroupUtil.isMmsGroup(groupId)); + if (admins != null) { + contentValues.put(ADMINS, Address.toSerializedList(admins, ',')); + } + databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { @@ -260,6 +266,17 @@ public class GroupDatabase extends Database { }); } + public void updateAdmins(String groupId, List
admins) { + Collections.sort(admins); + + ContentValues contents = new ContentValues(); + contents.put(ADMINS, Address.toSerializedList(admins, ',')); + contents.put(ACTIVE, 1); + + databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", + new String[] {groupId}); + } + public void remove(String groupId, Address source) { List
currentMembers = getCurrentMembers(groupId); currentMembers.remove(source); @@ -351,7 +368,8 @@ public class GroupDatabase extends Database { cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1, cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)), cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1, - cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL))); + cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)), + cursor.getString(cursor.getColumnIndexOrThrow(ADMINS))); } @Override @@ -375,10 +393,11 @@ public class GroupDatabase extends Database { private final boolean active; private final boolean mms; private final String url; + private final List
admins; public GroupRecord(String id, String title, String members, byte[] avatar, long avatarId, byte[] avatarKey, String avatarContentType, - String relay, boolean active, byte[] avatarDigest, boolean mms, String url) + String relay, boolean active, byte[] avatarDigest, boolean mms, String url, String admins) { this.id = id; this.title = title; @@ -394,6 +413,9 @@ public class GroupDatabase extends Database { if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ','); else this.members = new LinkedList<>(); + + if (!TextUtils.isEmpty(admins)) this.admins = Address.fromSerializedList(admins, ','); + else this.admins = new LinkedList<>(); } public byte[] getId() { @@ -453,5 +475,7 @@ public class GroupDatabase extends Database { public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); } public String getUrl() { return url; } + + public List
getAdmins() { return admins; } } } diff --git a/src/org/thoughtcrime/securesms/database/SmsMigrator.java b/src/org/thoughtcrime/securesms/database/SmsMigrator.java index 071714e1f1..f73acd496f 100644 --- a/src/org/thoughtcrime/securesms/database/SmsMigrator.java +++ b/src/org/thoughtcrime/securesms/database/SmsMigrator.java @@ -220,7 +220,7 @@ public class SmsMigrator { memberAddresses.add(recipient.getAddress()); } - String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true); + String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true, null); Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true); long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index d0b12cdd42..eab904116e 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -35,16 +35,19 @@ import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.loki.*; +import org.thoughtcrime.securesms.loki.LokiAPIDatabase; +import org.thoughtcrime.securesms.loki.LokiMessageDatabase; +import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; +import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; +import org.thoughtcrime.securesms.loki.LokiThreadDatabase; +import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.loki.api.LokiPublicChat; import java.io.File; -import java.security.acl.Group; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -547,6 +550,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { groupUpdate.put("group_id", newId); db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId }); } + + // Add admin field in groups + db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT"); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 8f5004fbe3..80034d3d80 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -55,11 +55,12 @@ public class GroupManager { @NonNull Set members, @Nullable Bitmap avatar, @Nullable String name, - boolean mms) + boolean mms, + @NonNull Set admins) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); String id = GroupUtil.getEncodedId(database.allocateGroupId(), mms); - return createGroup(id, context, members, avatar, name, mms); + return createGroup(id, context, members, avatar, name, mms, admins); } public static @NonNull GroupActionResult createGroup(@NonNull String id, @@ -67,21 +68,23 @@ public class GroupManager { @NonNull Set members, @Nullable Bitmap avatar, @Nullable String name, - boolean mms) + boolean mms, + @NonNull Set admins) { final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final String groupId = GroupUtil.getEncodedId(id.getBytes(), mms); final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); final Set
memberAddresses = getMemberAddresses(members); + final Set
adminAddresses = getMemberAddresses(admins); memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); - groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null); + groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses)); if (!mms) { groupDatabase.updateAvatar(groupId, avatarBytes); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); - return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); + return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); } else { long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); @@ -117,7 +120,7 @@ public class GroupManager { final Set
memberAddresses = new HashSet<>(); memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); - groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null); + groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>()); groupDatabase.updateAvatar(groupId, avatarBytes); @@ -129,20 +132,23 @@ public class GroupManager { @NonNull String groupId, @NonNull Set members, @Nullable Bitmap avatar, - @Nullable String name) + @Nullable String name, + @NonNull Set admins) throws InvalidNumberException { final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final Set
memberAddresses = getMemberAddresses(members); + final Set
adminAddresses = getMemberAddresses(admins); final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); + groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses)); groupDatabase.updateTitle(groupId, name); groupDatabase.updateAvatar(groupId, avatarBytes); if (!GroupUtil.isMmsGroup(groupId)) { - return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); + return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); } else { Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); @@ -154,7 +160,8 @@ public class GroupManager { @NonNull String groupId, @NonNull Set
members, @Nullable String groupName, - @Nullable byte[] avatar) + @Nullable byte[] avatar, + @NonNull Set
admins) { try { Attachment avatarAttachment = null; @@ -162,15 +169,20 @@ public class GroupManager { Recipient groupRecipient = Recipient.from(context, groupAddress, false); List numbers = new LinkedList<>(); - for (Address member : members) { numbers.add(member.serialize()); } + List adminNumbers = new LinkedList<>(); + for (Address admin : admins) { + adminNumbers.add(admin.serialize()); + } + GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId))) .setType(GroupContext.Type.UPDATE) - .addAllMembers(numbers); + .addAllMembers(numbers) + .addAllAdmins(adminNumbers); if (groupName != null) groupContextBuilder.setName(groupName); GroupContext groupContext = groupContextBuilder.build(); diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index f184231a72..1ff6467a36 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -25,12 +25,15 @@ import org.thoughtcrime.securesms.sms.IncomingGroupMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.loki.api.LokiStorageAPI; +import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import java.util.Collections; import java.util.HashSet; @@ -87,6 +90,7 @@ public class GroupMessageProcessor { SignalServiceAttachment avatar = group.getAvatar().orNull(); List
members = group.getMembers().isPresent() ? new LinkedList
() : null; + List
admins = group.getAdmins().isPresent() ? new LinkedList<>() : null; if (group.getMembers().isPresent()) { for (String member : group.getMembers().get()) { @@ -94,8 +98,22 @@ public class GroupMessageProcessor { } } + // We should only create the group if we are part of the member list + String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); + String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); + if (members == null || !members.contains(Address.fromSerialized(hexEncodedPublicKey))) { + Log.d("Loki - Group Message", "Received a group create message which doesn't include us in the member list. Ignoring."); + return null; + } + + if (group.getAdmins().isPresent()) { + for (String admin : group.getAdmins().get()) { + admins.add(Address.fromExternal(context, admin)); + } + } + database.create(id, group.getName().orNull(), members, - avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null); + avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins); return storeMessage(context, content, group, builder.build(), outgoing); } @@ -110,6 +128,21 @@ public class GroupMessageProcessor { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); String id = GroupUtil.getEncodedId(group); + // Only update group if admin sent the message + if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { + String sender = content.getSender(); + String hexEncodedPublicKey = sender; + try { + String primaryDevice = PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(sender), 5000).get(); + if (primaryDevice != null) { hexEncodedPublicKey = primaryDevice; } + } catch (Exception e) { } + + if (!groupRecord.getAdmins().contains(Address.fromSerialized(hexEncodedPublicKey))) { + Log.d("Loki - Group Message", "Received a group update message from a non-admin user for " + id +". Ignoring."); + return null; + } + } + Set
recordMembers = new HashSet<>(groupRecord.getMembers()); Set
messageMembers = new HashSet<>(); @@ -262,6 +295,10 @@ public class GroupMessageProcessor { builder.addAllMembers(group.getMembers().get()); } + if (group.getAdmins().isPresent()) { + builder.addAllAdmins(group.getAdmins().get()); + } + return builder; } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index ac6e206930..47411c08f0 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -244,7 +244,7 @@ public class MmsDownloadJob extends BaseJob { } if (members.size() > 2) { - group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true))); + group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true, new LinkedList<>()))); } IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 37b4c3ad8f..abe6dd862c 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -267,7 +267,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { GroupContext groupContext = groupMessage.getGroupContext(); SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; - SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar); + SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList()); SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .withExpiration(message.getRecipient().getExpireMessages()) diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index d35dd2925f..460b028932 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -97,15 +97,20 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType { } List members = new LinkedList<>(); - for (Address member : record.get().getMembers()) { members.add(member.serialize()); } + List admins = new LinkedList<>(); + for (Address admin : record.get().getAdmins()) { + admins.add(admin.serialize()); + } + SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE) .withAvatar(avatar) .withId(groupId, SignalServiceGroup.GroupType.SIGNAL) .withMembers(members) + .withAdmins(admins) .withName(record.get().getTitle()) .build(); diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt index 2667838ad8..d2456d86b4 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt @@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.jobs.PushDecryptJob import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.recipients.Recipient @@ -112,7 +111,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki // region Polling private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage { val id = group.id.toByteArray() - val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null) + val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null) val quote = if (message.quote != null) { SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf()) } else { diff --git a/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt index 30d3b7c269..c56f2a224d 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiRSSFeedPoller.kt @@ -64,7 +64,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF bodyAsHTML = matcher.replaceAll("$2 ($1)") val body = Html.fromHtml(bodyAsHTML).toString().trim() val id = feed.id.toByteArray() - val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null) + val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null) val x2 = SignalServiceDataMessage(timestamp, x1, null, body) val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false) PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent()) From 538cd39d00464a872ab47f8417032c05a314efd1 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 13 Dec 2019 11:24:47 +1100 Subject: [PATCH 04/19] Send group messages to members and their secondary devices. --- .../securesms/database/GroupDatabase.java | 4 ++- .../groups/GroupMessageProcessor.java | 27 ++++++++++++------- .../securesms/jobs/PushGroupSendJob.java | 17 +++++++++++- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 19108e703e..92c5e13262 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -152,7 +152,9 @@ public class GroupDatabase extends Database { if (!includeSelf && Util.isOwnNumber(context, member)) continue; - recipients.add(Recipient.from(context, member, false)); + if (member.isPhone()) { + recipients.add(Recipient.from(context, member, false)); + } } return recipients; diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 1ff6467a36..0c78bf185f 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -99,8 +99,7 @@ public class GroupMessageProcessor { } // We should only create the group if we are part of the member list - String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); + String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context)); if (members == null || !members.contains(Address.fromSerialized(hexEncodedPublicKey))) { Log.d("Loki - Group Message", "Received a group create message which doesn't include us in the member list. Ignoring."); return null; @@ -130,13 +129,7 @@ public class GroupMessageProcessor { // Only update group if admin sent the message if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { - String sender = content.getSender(); - String hexEncodedPublicKey = sender; - try { - String primaryDevice = PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(sender), 5000).get(); - if (primaryDevice != null) { hexEncodedPublicKey = primaryDevice; } - } catch (Exception e) { } - + String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender()); if (!groupRecord.getAdmins().contains(Address.fromSerialized(hexEncodedPublicKey))) { Log.d("Loki - Group Message", "Received a group update message from a non-admin user for " + id +". Ignoring."); return null; @@ -198,7 +191,10 @@ public class GroupMessageProcessor { @NonNull SignalServiceGroup group, @NonNull GroupRecord record) { - if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) { + String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender()); + String ourPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context)); + // If the requester is a group member and we are admin then we should send them the group update + if (record.getMembers().contains(Address.fromSerialized(hexEncodedPublicKey)) && record.getAdmins().contains(Address.fromSerialized(ourPublicKey))) { ApplicationContext.getInstance(context) .getJobManager() .add(new PushGroupUpdateJob(content.getSender(), group.getGroupId())); @@ -302,4 +298,15 @@ public class GroupMessageProcessor { return builder; } + private static String getMasterHexEncodedPublicKey(Context context, String hexEncodedPublicKey) { + String ourPublicKey = TextSecurePreferences.getLocalNumber(context); + try { + String masterHexEncodedPublicKey = hexEncodedPublicKey.equalsIgnoreCase(ourPublicKey) + ? TextSecurePreferences.getMasterHexEncodedPublicKey(context) + : PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(hexEncodedPublicKey), 5000).get(); + return masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : hexEncodedPublicKey; + } catch (Exception e) { + return hexEncodedPublicKey; + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index abe6dd862c..ee970fdaab 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -47,6 +47,8 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiStorageAPI; +import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import java.io.IOException; import java.util.ArrayList; @@ -313,7 +315,20 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList(); List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); - return Stream.of(members).map(Recipient::getAddress).toList(); + + // Add secondary devices to the list + Set
memberSet = Stream.of(members).map(Recipient::getAddress).collect(Collectors.toSet()); + for (Recipient member : members) { + if (!member.getAddress().isPhone()) { continue; } + try { + List secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.getAddress().serialize()), 5000).get(); + memberSet.addAll(Stream.of(secondaryDevices).map(Address::fromSerialized).toList()); + } catch (Exception e) { + // Timed out, go to the next member + } + } + + return new LinkedList<>(memberSet); } } From 93a9f4c1dcb8791d9d1804037c5c7a7b3f0f7de2 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 13 Dec 2019 16:04:24 +1100 Subject: [PATCH 05/19] Handle SessionRequest messages. --- .../securesms/GroupCreateActivity.java | 2 +- .../groups/GroupMessageProcessor.java | 30 ++++++++++++++++ .../securesms/jobs/PushDecryptJob.java | 36 +++++++++++++++---- .../securesms/jobs/PushGroupSendJob.java | 21 ++++++++++- .../loki/PushBackgroundMessageSendJob.kt | 11 +++++- .../securesms/sms/MessageSender.java | 9 +++-- 6 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index b35d8f77b9..d87946c97d 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -497,7 +497,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity for (Recipient recipient : recipients) { boolean isPush = isActiveInDirectory(recipient); - if (failIfNotPush && !isPush) { + if (failIfNotPush && !isPush && !recipient.getAddress().isPhone()) { results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group, recipient.toShortString()))); } else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) { diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 0c78bf185f..af5d0274af 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -8,6 +8,7 @@ import android.support.annotation.Nullable; import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; @@ -23,15 +24,18 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.IncomingGroupMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.utilities.PromiseUtil; @@ -41,6 +45,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import kotlin.Unit; + import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; @@ -114,6 +120,10 @@ public class GroupMessageProcessor { database.create(id, group.getName().orNull(), members, avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins); + if (group.getMembers().isPresent()) { + establishSessionsWithMembersIfNeeded(context, group.getMembers().get()); + } + return storeMessage(context, content, group, builder.build(), outgoing); } @@ -183,6 +193,10 @@ public class GroupMessageProcessor { if (!groupRecord.isActive()) database.setActive(id, true); + if (group.getMembers().isPresent()) { + establishSessionsWithMembersIfNeeded(context, group.getMembers().get()); + } + return storeMessage(context, content, group, builder.build(), outgoing); } @@ -309,4 +323,20 @@ public class GroupMessageProcessor { return hexEncodedPublicKey; } } + + private static void establishSessionsWithMembersIfNeeded(Context context, List members) { + String ourNumber = TextSecurePreferences.getLocalNumber(context); + for (String member : members) { + // Make sure we have session with all of the members secondary devices + LokiStorageAPI.shared.getAllDevicePublicKeys(member).success(devices -> { + if (devices.contains(ourNumber)) { return Unit.INSTANCE; } + for (String device : devices) { + SignalProtocolAddress protocolAddress = new SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID); + boolean haveSession = new TextSecureSessionStore(context).containsSession(protocolAddress); + if (!haveSession) { MessageSender.sendBackgroundSessionRequest(context, device); } + } + return Unit.INSTANCE; + }); + } + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 99e05c6565..6d08808ca2 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -42,7 +42,6 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; @@ -296,6 +295,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType { // Loki - Handle friend request acceptance if needed acceptFriendRequestIfNeeded(envelope, content); + // Loki - Session requests + handleSessionRequestIfNeeded(envelope, content); + // Loki - Store pre key bundle // We shouldn't store it if it's a pairing message if (!content.getPairingAuthorisation().isPresent()) { @@ -340,7 +342,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } else { // Loki - We shouldn't process session restore message any further - if (message.isSessionRestore()) { return; } + if (message.isSessionRestore() || message.isSessionRequest()) { return; } if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isExpirationUpdate()) @@ -697,7 +699,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Log.d("Loki", "Sent friend request to " + pubKey); } else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { // Accept the incoming friend request - becomeFriendsWithContact(pubKey, false); + becomeFriendsWithContact(pubKey, false, false); // Send them an accept message back MessageSender.sendBackgroundMessage(context, pubKey); Log.d("Loki", "Became friends with " + deviceContact.getNumber()); @@ -1225,10 +1227,21 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { // If we get anything other than a friend request, we can assume that we have a session with the other user if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; } - becomeFriendsWithContact(content.getSender(), true); + becomeFriendsWithContact(content.getSender(), true, false); } - private void becomeFriendsWithContact(String pubKey, boolean syncContact) { + private void handleSessionRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { + if (envelope.isFriendRequest() && isSessionRequest(content)) { + // TODO: Check if member is in one of our private groups + boolean isInOneOfOurGroups = false; + if (isInOneOfOurGroups) { + // Send a background message to acknowledge session request + MessageSender.sendBackgroundMessage(context, content.getSender()); + } + } + } + + private void becomeFriendsWithContact(String pubKey, boolean syncContact, boolean force) { LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false); if (contactID.isGroupRecipient()) return; @@ -1236,6 +1249,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID); LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID); if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; } + + // We shouldn't be able to skip from None -> Friends in normal circumstances. + // Multi-device is the exception to this rule because we want to automatically be friends with a secondary device + if (!force && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; } + // If the thread's friend request status is not `FRIENDS`, but we're receiving a message, // it must be a friend request accepted message. Declining a friend request doesn't send a message. lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); @@ -1256,13 +1274,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { - if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; } + if (!envelope.isFriendRequest() || message.isGroupUpdate() || message.isSessionRequest()) { return; } // This handles the case where another user sends us a regular message without authorisation Promise promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000); boolean shouldBecomeFriends = PromiseUtil.get(promise, false); if (shouldBecomeFriends) { // Become friends AND update the message they sent - becomeFriendsWithContact(content.getSender(), true); + becomeFriendsWithContact(content.getSender(), true, true); // Send them an accept message back MessageSender.sendBackgroundMessage(context, content.getSender()); } else { @@ -1843,6 +1861,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType { return false; } + private boolean isSessionRequest(SignalServiceContent content) { + return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest(); + } + private boolean isGroupChatMessage(SignalServiceContent content) { return content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupInfo().isPresent(); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index ee970fdaab..cbb583f793 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo; @@ -30,9 +31,11 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; +import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; @@ -160,7 +163,23 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList(); else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId); - List results = deliver(message, target); + // Only send messages to the contacts we have sessions with + List
validTargets = Stream.of(target).filter(member -> { + SignalProtocolAddress protocolAddress = new SignalProtocolAddress(member.toPhoneString(), SignalServiceAddress.DEFAULT_DEVICE_ID); + boolean hasSession = new TextSecureSessionStore(context).containsSession(protocolAddress); + if (hasSession) { return true; } + + // We should allow sending if we have a prekeybundle for the contact + return DatabaseFactory.getLokiPreKeyBundleDatabase(context).hasPreKeyBundle(member.toPhoneString()); + }).toList(); + + // Send a session request to the other devices + List
others = Stream.of(target).filter(t -> !validTargets.contains(t)).toList(); + for (Address device : others) { + MessageSender.sendBackgroundSessionRequest(context, device.toPhoneString()); + } + + List results = deliver(message, validTargets); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList(); List identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList(); Set
successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet()); diff --git a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt index 73ac0a9144..8185e071cd 100644 --- a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt @@ -13,7 +13,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.internal.util.JsonUtil import java.io.IOException -import java.lang.IllegalStateException import java.util.concurrent.TimeUnit data class BackgroundMessage private constructor(val data: Map) { @@ -37,6 +36,12 @@ data class BackgroundMessage private constructor(val data: Map) { "friendRequest" to true, "sessionRestore" to true )) + @JvmStatic + fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf( + "recipient" to recipient, + "friendRequest" to true, + "sessionRequest" to true + )) internal fun parse(serialized: String): BackgroundMessage { val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map ?: throw AssertionError("JSON parsing failed") @@ -103,6 +108,10 @@ class PushBackgroundMessageSendJob private constructor( dataMessage.asSessionRestore(true) } + if (message.get("sessionRequest", false)) { + dataMessage.asSessionRequest(true) + } + val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val address = SignalServiceAddress(recipient) try { diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 744a81c39a..dd18a56b80 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -17,7 +17,6 @@ package org.thoughtcrime.securesms.sms; import android.content.Context; -import android.os.AsyncTask; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.ApplicationContext; @@ -60,11 +59,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.ContactTokenDetails; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.utilities.PromiseUtil; @@ -143,6 +138,10 @@ public class MessageSender { public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) { ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey))); } + + public static void sendBackgroundSessionRequest(Context context, String contactHexEncodedPublicKey) { + ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRequest(contactHexEncodedPublicKey))); + } // endregion public static long send(final Context context, From 6b38e5d799cd8a4bb4e15fc638202debbc4bbaca Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 22 Jan 2020 09:18:12 +1100 Subject: [PATCH 06/19] Get group record programatically instead of making user pass in the recipient. This was the original function but i added in recipient to it. This is causing crashes and thus we just get the group record. --- .../conversation/ConversationUpdateItem.java | 2 +- .../database/model/MessageRecord.java | 2 +- .../securesms/util/GroupUtil.java | 41 +++++++++++-------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 5e3e103d23..2d8c98bee0 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -175,7 +175,7 @@ public class ConversationUpdateItem extends LinearLayout icon.setImageResource(R.drawable.ic_group_grey600_24dp); icon.clearColorFilter(); - GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.getRecipient()).addListener(this); + GroupUtil.getDescription(getContext(), messageRecord.getBody()).addListener(this); body.setText(messageRecord.getDisplayBody(getContext())); title.setVisibility(GONE); diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index a2b324c7cb..0531562b73 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -92,7 +92,7 @@ public abstract class MessageRecord extends DisplayRecord { if (isGroupUpdate() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); } else if (isGroupUpdate()) { - return new SpannableString(GroupUtil.getDescription(context, getBody(), getRecipient()).toString(getIndividualRecipient())); + return new SpannableString(GroupUtil.getDescription(context, getBody()).toString(getIndividualRecipient())); } else if (isGroupQuit() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_left_group)); } else if (isGroupQuit()) { diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 3dc6c363a7..9268e78264 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -7,10 +7,8 @@ import android.support.annotation.WorkerThread; import com.google.protobuf.ByteString; -import network.loki.messenger.BuildConfig; import network.loki.messenger.R; import org.thoughtcrime.securesms.database.Address; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase.*; @@ -114,25 +112,17 @@ public class GroupUtil { } - public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, @Nullable Recipient groupRecipient) { - // Make sure we always are passing a group recipient - if (BuildConfig.DEBUG && groupRecipient != null && !groupRecipient.isGroupRecipient()) { - throw new AssertionError(); - } - + public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) { if (encodedGroup == null) { - return new GroupDescription(context, null, null); + return new GroupDescription(context, null); } try { GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); - GroupRecord groupRecord = groupRecipient != null - ? DatabaseFactory.getGroupDatabase(context).getGroup(groupRecipient.getAddress().toGroupString()).orNull() - : null; - return new GroupDescription(context, groupContext, groupRecord); + return new GroupDescription(context, groupContext); } catch (IOException e) { Log.w(TAG, e); - return new GroupDescription(context, null, null); + return new GroupDescription(context, null); } } @@ -144,8 +134,7 @@ public class GroupUtil { private final List removedMembers; private boolean ourDeviceWasRemoved; - public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { this(context, groupContext, null); } - public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext, @Nullable GroupRecord groupRecord) { + public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { this.context = context.getApplicationContext(); this.groupContext = groupContext; @@ -155,7 +144,7 @@ public class GroupUtil { if (groupContext != null && !groupContext.getMembersList().isEmpty()) { List memberList = groupContext.getMembersList(); - List
currentMembers = groupRecord != null ? groupRecord.getMembers() : null; + List
currentMembers = getCurrentGroupMembers(); // Add them to the member or removed members lists for (String member : memberList) { @@ -234,5 +223,23 @@ public class GroupUtil { return result; } + + private List
getCurrentGroupMembers() { + if (groupContext == null) { return null; } + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + byte[] decodedGroupId = groupContext.getId().toByteArray(); + String signalGroupId = getEncodedId(decodedGroupId, false); + String publicChatId = getEncodedPublicChatId(decodedGroupId); + String rssFeedId = getEncodedRSSFeedId(decodedGroupId); + GroupRecord groupRecord = null; + if (!groupDatabase.isUnknownGroup(signalGroupId)) { + groupRecord = groupDatabase.getGroup(signalGroupId).orNull(); + } else if (!groupDatabase.isUnknownGroup(publicChatId)) { + groupRecord = groupDatabase.getGroup(publicChatId).orNull(); + } else if (!groupDatabase.isUnknownGroup(rssFeedId)) { + groupRecord = groupDatabase.getGroup(rssFeedId).orNull(); + } + return (groupRecord != null) ? groupRecord.getMembers() : null; + } } } From 998d03b258ae9c4783119f93d4d8ad2b84e77ad5 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 31 Jan 2020 13:57:24 +1100 Subject: [PATCH 07/19] Implement create closed group screen --- AndroidManifest.xml | 3 + res/drawable/ic_circle.xml | 6 ++ res/layout/activity_create_closed_group.xml | 30 ++++++ res/layout/activity_home.xml | 26 ++++-- res/layout/view_user.xml | 53 +++++++++++ res/menu/menu_create_closed_group.xml | 10 ++ .../activities/CreateClosedGroupActivity.kt | 93 +++++++++++++++++++ .../activities/CreateClosedGroupAdapter.kt | 48 ++++++++++ .../activities/CreateClosedGroupLoader.kt | 24 +++++ .../loki/redesign/activities/HomeActivity.kt | 6 ++ .../securesms/loki/redesign/views/UserView.kt | 52 +++++++++++ 11 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 res/drawable/ic_circle.xml create mode 100644 res/layout/activity_create_closed_group.xml create mode 100644 res/layout/view_user.xml create mode 100644 res/menu/menu_create_closed_group.xml create mode 100644 src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt create mode 100644 src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt create mode 100644 src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt create mode 100644 src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e4693b99ed..0e5a5d0654 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -158,6 +158,9 @@ + + + diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml new file mode 100644 index 0000000000..fa6585f797 --- /dev/null +++ b/res/layout/activity_create_closed_group.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml index d03b9cb65d..28e927d94d 100644 --- a/res/layout/activity_home.xml +++ b/res/layout/activity_home.xml @@ -40,13 +40,27 @@ android:layout_centerVertical="true" android:layout_marginLeft="64dp" /> - + android:layout_centerVertical="true"> + + + + + + diff --git a/res/layout/view_user.xml b/res/layout/view_user.xml new file mode 100644 index 0000000000..6c63b7759c --- /dev/null +++ b/res/layout/view_user.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/menu_create_closed_group.xml b/res/menu/menu_create_closed_group.xml new file mode 100644 index 0000000000..75b41dcd68 --- /dev/null +++ b/res/menu/menu_create_closed_group.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt new file mode 100644 index 0000000000..056d8b7296 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt @@ -0,0 +1,93 @@ +package org.thoughtcrime.securesms.loki.redesign.activities + +import android.os.Bundle +import android.support.v4.app.LoaderManager +import android.support.v4.content.Loader +import android.support.v7.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import kotlinx.android.synthetic.main.activity_linked_devices.* +import kotlinx.android.synthetic.main.view_user.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.mms.GlideApp + +class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberClickListener, LoaderManager.LoaderCallbacks> { + private var members = listOf() + set(value) { field = value; createClosedGroupAdapter.members = value } + + private val createClosedGroupAdapter by lazy { + val result = CreateClosedGroupAdapter(this) + result.glide = GlideApp.with(this) + result.memberClickListener = this + result + } + + private val selectedMembers: Set + get() { return createClosedGroupAdapter.selectedMembers } + + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { + super.onCreate(savedInstanceState, isReady) + setContentView(R.layout.activity_create_closed_group) + supportActionBar!!.title = "New Closed Group" + recyclerView.adapter = createClosedGroupAdapter + recyclerView.layoutManager = LinearLayoutManager(this) + LoaderManager.getInstance(this).initLoader(0, null, this) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_create_closed_group, menu) + return true + } + // endregion + + // region Updating + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader> { + return CreateClosedGroupLoader(this) + } + + override fun onLoadFinished(loader: Loader>, members: List) { + update(members) + } + + override fun onLoaderReset(loader: Loader>) { + update(listOf()) + } + + private fun update(members: List) { + this.members = members + } + // endregion + + // region Interaction + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + when(id) { + R.id.createClosedGroupButton -> createClosedGroup() + else -> { /* Do nothing */ } + } + return super.onOptionsItemSelected(item) + } + + override fun onMemberClick(member: String) { + createClosedGroupAdapter.onMemberClick(member) + } + + private fun createClosedGroup() { + val name = nameTextView.text.trim() + if (name.isEmpty()) { + return Toast.makeText(this, "Please enter a group name", Toast.LENGTH_LONG).show() + } + if (name.length >= 64) { + return Toast.makeText(this, "Please enter a shorter group name", Toast.LENGTH_LONG).show() + } + val selectedMembers = this.selectedMembers + if (selectedMembers.count() < 2) { + return Toast.makeText(this, "Please pick at least 2 group members", Toast.LENGTH_LONG).show() + } + // TODO: Create group + } + // endregion +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt new file mode 100644 index 0000000000..bd8c6eff02 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.loki.redesign.activities + +import android.content.Context +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import org.thoughtcrime.securesms.loki.redesign.views.UserView +import org.thoughtcrime.securesms.mms.GlideRequests + +class CreateClosedGroupAdapter(private val context: Context) : RecyclerView.Adapter() { + lateinit var glide: GlideRequests + val selectedMembers = mutableSetOf() + var members = listOf() + set(value) { field = value; notifyDataSetChanged() } + var memberClickListener: MemberClickListener? = null + + class ViewHolder(val view: UserView) : RecyclerView.ViewHolder(view) + + override fun getItemCount(): Int { + return members.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = UserView(context) + return ViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val member = members[position] + viewHolder.view.setOnClickListener { memberClickListener?.onMemberClick(member) } + val isSelected = selectedMembers.contains(member) + viewHolder.view.bind(member, isSelected, glide) + } + + fun onMemberClick(member: String) { + if (selectedMembers.contains(member)) { + selectedMembers.remove(member) + } else { + selectedMembers.add(member) + } + val index = members.indexOf(member) + notifyItemChanged(index) + } +} + +interface MemberClickListener { + + fun onMemberClick(member: String) +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt new file mode 100644 index 0000000000..60f5ce9f41 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.loki.redesign.activities + +import android.content.Context +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.AsyncLoader +import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus + +class CreateClosedGroupLoader(context: Context) : AsyncLoader>(context) { + + override fun loadInBackground(): List { + val threadDatabase = DatabaseFactory.getThreadDatabase(context) + val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) + val cursor = threadDatabase.conversationList + val reader = threadDatabase.readerFor(cursor) + val result = mutableListOf() + while (reader.next != null) { + val thread = reader.current + if (thread.recipient.isGroupRecipient) { continue } + if (lokiThreadDatabase.getFriendRequestStatus(thread.threadId) != LokiThreadFriendRequestStatus.FRIENDS) { continue } + result.add(thread.recipient.address.toString()) + } + return result + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt index 798528dbbe..bb24b04dac 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt @@ -83,6 +83,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe profileButton.hexEncodedPublicKey = hexEncodedPublicKey profileButton.update() profileButton.setOnClickListener { openSettings() } + createClosedGroupButton.setOnClickListener { createClosedGroup() } joinPublicChatButton.setOnClickListener { joinPublicChat() } // Set up seed reminder view val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null) @@ -182,6 +183,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe show(intent) } + private fun createClosedGroup() { + val intent = Intent(this, CreateClosedGroupActivity::class.java) + show(intent) + } + private fun joinPublicChat() { val intent = Intent(this, JoinPublicChatActivity::class.java) show(intent) diff --git a/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt b/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt new file mode 100644 index 0000000000..2b6e6536f7 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.loki.redesign.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView +import kotlinx.android.synthetic.main.view_user.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.recipients.Recipient + +class UserView : LinearLayout { + var user: String? = null + + // region Lifecycle + constructor(context: Context) : super(context) { + setUpViewHierarchy() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + setUpViewHierarchy() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + setUpViewHierarchy() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { + setUpViewHierarchy() + } + + private fun setUpViewHierarchy() { + val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val contentView = inflater.inflate(R.layout.view_user, null) + addView(contentView) + } + // endregion + + // region Updating + fun bind(user: String, isSelected: Boolean, glide: GlideRequests) { + profilePictureView.hexEncodedPublicKey = user + profilePictureView.additionalHexEncodedPublicKey = null + profilePictureView.isRSSFeed = false + profilePictureView.glide = glide + profilePictureView.update() + nameTextView.text = Recipient.from(context, Address.fromSerialized(user), false).name ?: "Unknown Contact" + tickImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) + } + // endregion +} \ No newline at end of file From 7610314165eb2a0215c819ddad9dffe104baf6aa Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 31 Jan 2020 14:23:53 +1100 Subject: [PATCH 08/19] Exclude linked devices from group member options --- .../redesign/activities/CreateClosedGroupLoader.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt index 60f5ce9f41..620d47b5c6 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.redesign.activities import android.content.Context import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.util.AsyncLoader +import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus class CreateClosedGroupLoader(context: Context) : AsyncLoader>(context) { @@ -10,6 +11,12 @@ class CreateClosedGroupLoader(context: Context) : AsyncLoader>(cont override fun loadInBackground(): List { val threadDatabase = DatabaseFactory.getThreadDatabase(context) val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) + val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) + val deviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey) + val userLinkedDeviceHexEncodedPublicKeys = deviceLinks.flatMap { + listOf( it.primaryDevicePublicKey.toLowerCase(), it.secondaryDevicePublicKey.toLowerCase() ) + }.toMutableSet() + userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey.toLowerCase()) val cursor = threadDatabase.conversationList val reader = threadDatabase.readerFor(cursor) val result = mutableListOf() @@ -17,7 +24,9 @@ class CreateClosedGroupLoader(context: Context) : AsyncLoader>(cont val thread = reader.current if (thread.recipient.isGroupRecipient) { continue } if (lokiThreadDatabase.getFriendRequestStatus(thread.threadId) != LokiThreadFriendRequestStatus.FRIENDS) { continue } - result.add(thread.recipient.address.toString()) + val hexEncodedPublicKey = thread.recipient.address.toString().toLowerCase() + if (userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey)) { continue } + result.add(hexEncodedPublicKey) } return result } From a7ea2f01a2fc674a3922e33b622849742dda6954 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 31 Jan 2020 16:15:04 +1100 Subject: [PATCH 09/19] Don't use envelope to determine friend request --- src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index de0af32553..393c1989e3 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -292,7 +292,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { acceptFriendRequestIfNeeded(content); // Loki - Session requests - handleSessionRequestIfNeeded(envelope, content); + handleSessionRequestIfNeeded(content); // Loki - Store pre key bundle // We shouldn't store it if it's a pairing message @@ -1226,8 +1226,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { becomeFriendsWithContact(content.getSender(), true, false); } - private void handleSessionRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { - if (envelope.isFriendRequest() && isSessionRequest(content)) { + private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) { + if (content.isFriendRequest() && isSessionRequest(content)) { // TODO: Check if member is in one of our private groups boolean isInOneOfOurGroups = false; if (isInOneOfOurGroups) { From d5420de0b744addf24d25f215093bffbfa64b759 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 3 Feb 2020 10:23:24 +1100 Subject: [PATCH 10/19] Hookup group creation to activity --- .../activities/CreateClosedGroupActivity.kt | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt index 056d8b7296..dbf5988a6f 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.loki.redesign.activities +import android.content.Intent +import android.graphics.Bitmap +import android.os.AsyncTask import android.os.Bundle import android.support.v4.app.LoaderManager import android.support.v4.content.Loader @@ -7,11 +10,19 @@ import android.support.v7.widget.LinearLayoutManager import android.view.Menu import android.view.MenuItem import android.widget.Toast -import kotlinx.android.synthetic.main.activity_linked_devices.* -import kotlinx.android.synthetic.main.view_user.* +import kotlinx.android.synthetic.main.activity_create_closed_group.* +import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.util.guava.Optional +import java.lang.ref.WeakReference class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberClickListener, LoaderManager.LoaderCallbacks> { private var members = listOf() @@ -76,7 +87,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC } private fun createClosedGroup() { - val name = nameTextView.text.trim() + val name = nameEditText.text.trim() if (name.isEmpty()) { return Toast.makeText(this, "Please enter a group name", Toast.LENGTH_LONG).show() } @@ -84,10 +95,57 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC return Toast.makeText(this, "Please enter a shorter group name", Toast.LENGTH_LONG).show() } val selectedMembers = this.selectedMembers - if (selectedMembers.count() < 2) { - return Toast.makeText(this, "Please pick at least 2 group members", Toast.LENGTH_LONG).show() + if (selectedMembers.count() < 1) { + return Toast.makeText(this, "Please pick at least 1 group member", Toast.LENGTH_LONG).show() + } + val recipients = selectedMembers.map { + Recipient.from(this, Address.fromSerialized(it), false) + }.toSet() + val local = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false) + CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + + private fun handleOpenConversation(threadId: Long, recipient: Recipient) { + val intent = Intent(this, ConversationActivity::class.java) + intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) + intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) + startActivity(intent) + finish() + } + // endregion + + // region Tasks + internal class CreateClosedGroupTask( + private val activity: WeakReference, + private val avatar: Bitmap?, + private val name: String?, + private val members: Set, + private val admins: Set + ) : AsyncTask>() { + + override fun doInBackground(vararg params: Void?): Optional { + val activity = activity.get() ?: return Optional.absent() + return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins)) + } + + override fun onPostExecute(result: Optional) { + val activity = activity.get() + if (activity == null) { + super.onPostExecute(result) + return + } + + if (result.isPresent && result.get().threadId > -1) { + if (!activity.isFinishing) { + activity.handleOpenConversation(result.get().threadId, result.get().groupRecipient) + } + } else { + super.onPostExecute(result) + Toast.makeText(activity.applicationContext, + R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show() + } } - // TODO: Create group } // endregion } \ No newline at end of file From 353f456ae2c1157b310bf61f32c887c38ff39a8a Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 3 Feb 2020 10:52:15 +1100 Subject: [PATCH 11/19] Accept session request if it was sent by a member of our group or if it was sent by one of our friends. --- .../securesms/database/GroupDatabase.java | 19 ++++++++++++++++++ .../securesms/jobs/PushDecryptJob.java | 20 +++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 92c5e13262..ebba2103ba 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -160,6 +160,23 @@ public class GroupDatabase extends Database { return recipients; } + public boolean signalGroupsHaveMember(String hexEncodedPublicKey) { + try { + Address address = Address.fromSerialized(hexEncodedPublicKey); + Reader reader = DatabaseFactory.getGroupDatabase(context).getGroups(); + GroupRecord record; + while ((record = reader.getNext()) != null) { + if (record.isSignalGroup() && record.members.contains(address)) { + return true; + } + } + + return false; + } catch (Exception e) { + return false; + } + } + public void create(@NonNull String groupId, @Nullable String title, @NonNull List
members, @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins) { @@ -476,6 +493,8 @@ public class GroupDatabase extends Database { public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); } + public boolean isSignalGroup() { return Address.fromSerialized(id).isSignalGroup(); } + public String getUrl() { return url; } public List
getAdmins() { return admins; } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 393c1989e3..1ea8bc924d 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -1228,12 +1228,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) { if (content.isFriendRequest() && isSessionRequest(content)) { - // TODO: Check if member is in one of our private groups - boolean isInOneOfOurGroups = false; - if (isInOneOfOurGroups) { - // Send a background message to acknowledge session request - MessageSender.sendBackgroundMessage(context, content.getSender()); - } + // Check if the session request from a member in one of our groups or our friend + LokiStorageAPI.shared.getPrimaryDevicePublicKey(content.getSender()).success(primaryDevicePublicKey -> { + String sender = primaryDevicePublicKey != null ? primaryDevicePublicKey : content.getSender(); + long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(sender), false)); + LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID); + boolean isOurFriend = threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS; + boolean isInOneOfOurGroups = DatabaseFactory.getGroupDatabase(context).signalGroupsHaveMember(sender); + boolean shouldAcceptSessionRequest = isOurFriend || isInOneOfOurGroups; + if (shouldAcceptSessionRequest) { + // Send a background message to acknowledge session request + MessageSender.sendBackgroundMessage(context, content.getSender()); + } + return Unit.INSTANCE; + }); } } From 60aa7f6ae3ddffff7e14e8d8451c8ec582370cb8 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 3 Feb 2020 12:08:11 +1100 Subject: [PATCH 12/19] Fix friend request accept message showing up in group chats. Fix crash. --- .../securesms/database/GroupDatabase.java | 2 +- .../securesms/jobs/PushDecryptJob.java | 16 ++++++++-------- .../securesms/sms/MessageSender.java | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index ebba2103ba..502a566f02 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -68,7 +68,7 @@ public class GroupDatabase extends Database { ACTIVE + " INTEGER DEFAULT 1, " + AVATAR_DIGEST + " BLOB, " + AVATAR_URL + " TEXT, " + - ADMINS + "TEXT, " + + ADMINS + " TEXT, " + MMS + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 1ea8bc924d..980e74020a 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -852,7 +852,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Optional sticker = getStickerAttachment(message.getSticker()); // If message is from group then we need to map it to the correct sender - Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); + Address sender = message.isGroupMessage() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), quote, sharedContacts, linkPreviews, sticker); @@ -1037,7 +1037,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice()); // If message is from group then we need to map it to the correct sender - Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); + Address sender = message.isGroupMessage() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); IncomingTextMessage _textMessage = new IncomingTextMessage(sender, content.getSenderDevice(), message.getTimestamp(), body, @@ -1278,7 +1278,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { - if (!content.isFriendRequest() || message.isGroupUpdate() || message.isSessionRequest()) { return; } + if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; } // This handles the case where another user sends us a regular message without authorisation Promise promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000); boolean shouldBecomeFriends = PromiseUtil.get(promise, false); @@ -1743,7 +1743,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private Recipient getSyncMessageDestination(SentTranscriptMessage message) { - if (message.getMessage().getGroupInfo().isPresent()) { + if (message.getMessage().isGroupMessage()) { return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false); } else { return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false); @@ -1751,7 +1751,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) { - if (message.getMessage().getGroupInfo().isPresent()) { + if (message.getMessage().isGroupMessage()) { return getSyncMessageDestination(message); } else { return getPrimaryDeviceRecipient(message.getDestination().get()); @@ -1759,7 +1759,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.getGroupInfo().isPresent()) { + if (message.isGroupMessage()) { return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false); } else { return Recipient.from(context, Address.fromSerialized(content.getSender()), false); @@ -1767,7 +1767,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.getGroupInfo().isPresent()) { + if (message.isGroupMessage()) { return getMessageDestination(content, message); } else { return getPrimaryDeviceRecipient(content.getSender()); @@ -1866,7 +1866,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private boolean isGroupChatMessage(SignalServiceContent content) { - return content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupInfo().isPresent(); + return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage(); } private void resetRecipientToPush(@NonNull Recipient recipient) { diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index dd18a56b80..df26a6ec03 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -201,7 +201,7 @@ public class MessageSender { if (attachment != null) { message.getAttachments().add(attachment); } long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); // Loki - Set the message's friend request status as soon as it has hit the database - if (message.isFriendRequest && !recipient.getAddress().isGroup()) { + if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) { FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId); } sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); @@ -214,7 +214,7 @@ public class MessageSender { try { long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); // Loki - Set the message's friend request status as soon as it has hit the database - if (message.isFriendRequest && !recipient.getAddress().isGroup()) { + if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) { FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId); } sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); From 44a0a428f54478aedf5478dd807265f2c924c04a Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 3 Feb 2020 13:53:58 +1100 Subject: [PATCH 13/19] Don't send a friend request message to a group. --- .../securesms/conversation/ConversationActivity.java | 4 ++-- src/org/thoughtcrime/securesms/jobs/TypingSendJob.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 16288cd2e6..c247c95f90 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2452,7 +2452,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity // Loki - Send a friend request if we're not yet friends with the user in question LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId); - outgoingMessage.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...) + outgoingMessage.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...) Permissions.with(this) .request(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS) @@ -2512,7 +2512,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity // Loki - Send a friend request if we're not yet friends with the user in question LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId); - message.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...) + message.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...) Permissions.with(this) .request(Manifest.permission.SEND_SMS) diff --git a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java index babe7fe04e..c6ecc30e2f 100644 --- a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import java.util.Collections; @@ -101,6 +102,10 @@ public class TypingSendJob extends BaseJob implements InjectableType { // Loki - Don't send typing indicators in group chats or to ourselves if (recipient.isGroupRecipient()) { return; } + + LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId); + if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; } + boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false); if (!isOurDevice) { messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage); From a61f9b4eb76f2fedcb9a198c098b94196c0fe3eb Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 3 Feb 2020 16:00:05 +1100 Subject: [PATCH 14/19] Don't set friend request on messages to ourselves --- src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt index a8d9cae2eb..e59a2875a9 100644 --- a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt +++ b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki import android.content.Context import nl.komponents.kovenant.ui.successUi import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus @@ -62,7 +63,7 @@ object FriendRequestHandler { fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) { if (threadId < 0) { return } val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return - if (!recipient.address.isPhone) { return } + if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return } val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId) val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) @@ -81,7 +82,7 @@ object FriendRequestHandler { // We only want to update the last message status if we're not friends with any of their linked devices // This ensures that we don't spam the UI with accept/decline messages val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return - if (!recipient.address.isPhone) { return } + if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return } isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends -> if (isFriends) { return@successUi } From 1ec1717c9984593b6b31c672bfe467ece002abb5 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 3 Feb 2020 21:08:09 +1100 Subject: [PATCH 15/19] Fix typo --- .../securesms/ApplicationContext.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 779186e744..2791de73b1 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -534,18 +534,18 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc public void createDefaultPublicChatsIfNeeded() { List defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); - for (LokiPublicChat publiChat : defaultPublicChats) { - long threadID = GroupManager.getPublicChatThreadId(publiChat.getId(), this); - String migrationKey = publiChat.getId() + "_migrated"; + for (LokiPublicChat publicChat : defaultPublicChats) { + long threadID = GroupManager.getPublicChatThreadId(publicChat.getId(), this); + String migrationKey = publicChat.getId() + "_migrated"; boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); - boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publiChat.getId()); - if (!isChatSetUp || !publiChat.isDeletable()) { - lokiPublicChatManager.addChat(publiChat.getServer(), publiChat.getChannel(), publiChat.getDisplayName()); - TextSecurePreferences.markChatSetUp(this, publiChat.getId()); + boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId()); + if (!isChatSetUp || !publicChat.isDeletable()) { + lokiPublicChatManager.addChat(publicChat.getServer(), publicChat.getChannel(), publicChat.getDisplayName()); + TextSecurePreferences.markChatSetUp(this, publicChat.getId()); TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } else if (threadID > -1 && !isChatMigrated) { // Migrate the old public chats - DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publiChat, threadID); + DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publicChat, threadID); TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } } From 9d0f40a18f83c12cdba70873d43099cea5268b01 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 4 Feb 2020 08:31:38 +1100 Subject: [PATCH 16/19] Allow leaving from a secondary device. --- .../securesms/conversation/ConversationActivity.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index c247c95f90..001273040b 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1152,10 +1152,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (threadId != -1 && leaveMessage.isPresent()) { MessageSender.send(this, leaveMessage.get(), threadId, false, null); + // We need to remove the master device from the group + String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this); + String localNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(this); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this); String groupId = groupRecipient.getAddress().toGroupString(); groupDatabase.setActive(groupId, false); - groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this))); + groupDatabase.remove(groupId, Address.fromSerialized(localNumber)); initializeEnabledCheck(); } else { From cc369f5c52fba21ec34c1d7896959534fc754c20 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 4 Feb 2020 10:09:32 +1100 Subject: [PATCH 17/19] Fix message syncing in group chats --- .../securesms/jobs/PushGroupSendJob.java | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index cbb583f793..2bc5241f58 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -35,6 +36,7 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -56,6 +58,7 @@ import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -163,8 +166,13 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList(); else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId); + String localNumber = TextSecurePreferences.getLocalNumber(context); + // Only send messages to the contacts we have sessions with List
validTargets = Stream.of(target).filter(member -> { + // Our device is always valid + if (member.serialize().equalsIgnoreCase(localNumber)) { return true; } + SignalProtocolAddress protocolAddress = new SignalProtocolAddress(member.toPhoneString(), SignalServiceAddress.DEFAULT_DEVICE_ID); boolean hasSession = new TextSecureSessionStore(context).containsSession(protocolAddress); if (hasSession) { return true; } @@ -330,18 +338,37 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } return result; } else { + /* + Our biggest assumption here is that group members will only consist of primary devices. + No secondary device should be able to be added to a group. + */ List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); - if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList(); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); + Set
memberSet = new HashSet<>(); + if (destinations.isEmpty()) { + List groupMembers = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); + memberSet.addAll(Stream.of(groupMembers).map(Recipient::getAddress).toList()); + } else { + memberSet.addAll(Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList()); + } - // Add secondary devices to the list - Set
memberSet = Stream.of(members).map(Recipient::getAddress).collect(Collectors.toSet()); - for (Recipient member : members) { - if (!member.getAddress().isPhone()) { continue; } + // Replace primary device public key with ours so message syncing works correctly + String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); + String localNumber = TextSecurePreferences.getLocalNumber(context); + if (masterHexEncodedPublicKey != null && memberSet.contains(Address.fromSerialized(masterHexEncodedPublicKey))) { + memberSet.remove(Address.fromSerialized(masterHexEncodedPublicKey)); + memberSet.add(Address.fromSerialized(localNumber)); + } + + // Add secondary devices to the list. We shouldn't add our secondary devices + for (Address member : memberSet) { + if (!member.isPhone() || member.serialize().equalsIgnoreCase(localNumber)) { continue; } try { - List secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.getAddress().serialize()), 5000).get(); - memberSet.addAll(Stream.of(secondaryDevices).map(Address::fromSerialized).toList()); + List secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.serialize()), 5000).get(); + memberSet.addAll(Stream.of(secondaryDevices).map(string -> { + // Loki - Calling .map(Address::fromSerialized) is causing errors, thus we use the long method :( + return Address.fromSerialized(string); + }).toList()); } catch (Exception e) { // Timed out, go to the next member } From db9f26d9df305f8a1cd46c0e9888f2d3ede4e4ba Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 4 Feb 2020 10:25:46 +1100 Subject: [PATCH 18/19] Map text & media group messages from secondary deivces to their primary device --- .../groups/GroupMessageProcessor.java | 6 ++++-- .../securesms/jobs/PushDecryptJob.java | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index af5d0274af..9ba4a0b043 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -70,6 +70,7 @@ public class GroupMessageProcessor { String id = GroupUtil.getEncodedId(group); Optional record = database.getGroup(id); + // TODO: Allow processing messages from secondary devices if (record.isPresent() && group.getType() == Type.UPDATE) { return handleGroupUpdate(context, content, group, record.get(), outgoing); } else if (!record.isPresent() && group.getType() == Type.UPDATE) { @@ -230,8 +231,9 @@ public class GroupMessageProcessor { GroupContext.Builder builder = createGroupContext(group); builder.setType(GroupContext.Type.QUIT); - if (members.contains(Address.fromExternal(context, content.getSender()))) { - database.remove(id, Address.fromExternal(context, content.getSender())); + String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender()); + if (members.contains(Address.fromExternal(context, hexEncodedPublicKey))) { + database.remove(id, Address.fromExternal(context, hexEncodedPublicKey)); if (outgoing) database.setActive(id, false); return storeMessage(context, content, group, builder.build(), outgoing); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 980e74020a..cb15f2c4a5 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -851,8 +851,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional sticker = getStickerAttachment(message.getSticker()); - // If message is from group then we need to map it to the correct sender - Address sender = message.isGroupMessage() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); + Address sender = primaryDeviceRecipient.getAddress(); + + // If message is from group then we need to map it to get the sender of the message + if (message.isGroupMessage()) { + sender = getPrimaryDeviceRecipient(content.getSender()).getAddress(); + } + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), quote, sharedContacts, linkPreviews, sticker); @@ -1036,8 +1041,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } else { notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice()); - // If message is from group then we need to map it to the correct sender - Address sender = message.isGroupMessage() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress(); + Address sender = primaryDeviceRecipient.getAddress(); + + // If message is from group then we need to map it to get the sender of the message + if (message.isGroupMessage()) { + sender = getPrimaryDeviceRecipient(content.getSender()).getAddress(); + } + IncomingTextMessage _textMessage = new IncomingTextMessage(sender, content.getSenderDevice(), message.getTimestamp(), body, From 5183a765759bbe81b00694321b5c6f1899fa4f3c Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 4 Feb 2020 12:58:34 +1100 Subject: [PATCH 19/19] Minor bug fixes --- res/xml/network_security_configuration.xml | 1 + src/org/thoughtcrime/securesms/groups/GroupManager.java | 5 ++++- .../securesms/groups/GroupMessageProcessor.java | 1 - src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 6 ++++++ .../loki/redesign/activities/CreateClosedGroupActivity.kt | 3 ++- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/res/xml/network_security_configuration.xml b/res/xml/network_security_configuration.xml index f74d8df9fc..79ba86792a 100644 --- a/res/xml/network_security_configuration.xml +++ b/res/xml/network_security_configuration.xml @@ -4,6 +4,7 @@ imaginary.stream storage.seed1.loki.network storage.seed2.loki.network + public.loki.foundation:22023 127.0.0.1 \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 80034d3d80..bcd4521bb8 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -78,7 +78,10 @@ public class GroupManager { final Set
memberAddresses = getMemberAddresses(members); final Set
adminAddresses = getMemberAddresses(admins); - memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); + String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); + String ourNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); + + memberAddresses.add(Address.fromSerialized(ourNumber)); groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses)); if (!mms) { diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 9ba4a0b043..9f216e2fad 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -70,7 +70,6 @@ public class GroupMessageProcessor { String id = GroupUtil.getEncodedId(group); Optional record = database.getGroup(id); - // TODO: Allow processing messages from secondary devices if (record.isPresent() && group.getType() == Type.UPDATE) { return handleGroupUpdate(context, content, group, record.get(), outgoing); } else if (!record.isPresent() && group.getType() == Type.UPDATE) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index cb15f2c4a5..2f543a86bc 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -858,6 +858,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType { sender = getPrimaryDeviceRecipient(content.getSender()).getAddress(); } + // Ignore messages from ourselves + if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; } + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), quote, sharedContacts, linkPreviews, sticker); @@ -1048,6 +1051,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType { sender = getPrimaryDeviceRecipient(content.getSender()).getAddress(); } + // Ignore messages from ourselves + if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; } + IncomingTextMessage _textMessage = new IncomingTextMessage(sender, content.getSenderDevice(), message.getTimestamp(), body, diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt index dbf5988a6f..034176745a 100644 --- a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt @@ -101,7 +101,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC val recipients = selectedMembers.map { Recipient.from(this, Address.fromSerialized(it), false) }.toSet() - val local = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false) + val ourNumber = TextSecurePreferences.getMasterHexEncodedPublicKey(this) ?: TextSecurePreferences.getLocalNumber(this) + val local = Recipient.from(this, Address.fromSerialized(ourNumber), false) CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) }