package org.thoughtcrime.securesms.util; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.sms.MessageSender; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.List; import network.loki.messenger.R; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; 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"); } return Hex.fromStringCondensed(groupId.split("!", 2)[1]); } public static String getDecodedStringId(String groupId) throws IOException { byte[] id = getDecodedId(groupId); return new String(id); } public static boolean isEncodedGroup(@NonNull String groupId) { 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); } public static boolean isSignalGroup(@NonNull String groupId) { return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX); } @WorkerThread public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { String encodedGroupId = groupRecipient.getAddress().toGroupString(); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); if (!groupDatabase.isActive(encodedGroupId)) { Log.w(TAG, "Group has already been left."); return Optional.absent(); } ByteString decodedGroupId; try { decodedGroupId = ByteString.copyFrom(getDecodedId(encodedGroupId)); } catch (IOException e) { Log.w(TAG, "Failed to decode group ID.", e); return Optional.absent(); } GroupContext groupContext = GroupContext.newBuilder() .setId(decodedGroupId) .setType(GroupContext.Type.QUIT) .build(); return Optional.of(new OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList())); } public static boolean leaveGroup(@NonNull Context context, Recipient groupRecipient) { if (!groupRecipient.getAddress().isSignalGroup()) { return true; } long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); if (threadId < 0 || !leaveMessage.isPresent()) { return false; } MessageSender.send(context, leaveMessage.get(), threadId, false, null); // We need to remove the master device from the group String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); String localNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); String groupId = groupRecipient.getAddress().toGroupString(); groupDatabase.setActive(groupId, false); groupDatabase.remove(groupId, Address.fromSerialized(localNumber)); return true; } public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) { if (encodedGroup == null) { return new GroupDescription(context, null); } try { GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); return new GroupDescription(context, groupContext); } catch (IOException e) { Log.w(TAG, e); return new GroupDescription(context, null); } } public static class GroupDescription { @NonNull private final Context context; @Nullable private final GroupContext groupContext; private final List newMembers; private final List removedMembers; private boolean ourDeviceWasRemoved; public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { this.context = context.getApplicationContext(); this.groupContext = groupContext; this.newMembers = new LinkedList<>(); this.removedMembers = new LinkedList<>(); this.ourDeviceWasRemoved = false; if (groupContext != null) { List newMembers = groupContext.getNewMembersList(); for (String member : newMembers) { this.newMembers.add(this.toRecipient(member)); } List removedMembers = groupContext.getRemovedMembersList(); for (String member : removedMembers) { this.removedMembers.add(this.toRecipient(member)); } // Check if our device was removed if (!removedMembers.isEmpty()) { String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context); ourDeviceWasRemoved = removedMembers.contains(hexEncodedPublicKey); } } } private Recipient toRecipient(String hexEncodedPublicKey) { Address address = Address.fromSerialized(hexEncodedPublicKey); return Recipient.from(context, address, false); } 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())); if (groupContext == null) { return description.toString(); } String title = groupContext.getName(); if (!newMembers.isEmpty()) { description.append("\n"); description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group, newMembers.size(), toString(newMembers))); } 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()) { String separator = (!newMembers.isEmpty() || !removedMembers.isEmpty()) ? " " : "\n"; description.append(separator); description.append(context.getString(R.string.GroupUtil_group_name_is_now, title)); } return description.toString(); } public void addListener(RecipientModifiedListener listener) { if (!this.newMembers.isEmpty()) { for (Recipient member : this.newMembers) { member.addListener(listener); } } } private String toString(List recipients) { String result = ""; for (int i=0;i