Merge pull request #60 from loki-project/private-group-chat

Private group chat
This commit is contained in:
gmbnt 2020-02-04 13:23:47 +11:00 committed by GitHub
commit 75cd2d6d28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1063 additions and 212 deletions

View File

@ -158,6 +158,9 @@
<activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreatePrivateChatActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreateClosedGroupActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.JoinPublicChatActivity"
android:screenOrientation="portrait"

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="15"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M7.5,7.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0"
android:strokeColor="#FFFFFF" android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_session_background"
android:orientation="vertical">
<EditText
style="@style/SmallSessionEditText"
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/large_spacing"
android:hint="Enter a group name" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -40,13 +40,27 @@
android:layout_centerVertical="true"
android:layout_marginLeft="64dp" />
<ImageView
android:id="@+id/joinPublicChatButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_globe"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentRight="true"
android:layout_centerVertical="true" />
android:layout_centerVertical="true">
<ImageView
android:id="@+id/createClosedGroupButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_group_white_24dp" />
<ImageView
android:id="@+id/joinPublicChatButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="@dimen/medium_spacing"
android:src="@drawable/ic_globe" />
</LinearLayout>
</RelativeLayout>

53
res/layout/view_user.xml Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/conversation_view_background"
android:orientation="vertical">
<LinearLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="@dimen/medium_spacing">
<org.thoughtcrime.securesms.loki.redesign.views.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />
<TextView
android:id="@+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/medium_spacing"
android:maxLines="1"
android:ellipsize="end"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Spiderman" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<ImageView
android:id="@+id/tickImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="@dimen/medium_spacing"
android:src="@drawable/ic_circle" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="Done"
android:id="@+id/createClosedGroupButton"
app:showAsAction="always" />
</menu>

View File

@ -1073,7 +1073,12 @@
<item quantity="one">%1$s joined the group.</item>
<item quantity="other">%1$s joined the group.</item>
</plurals>
<plurals name="GroupUtil_removed_from_the_group">
<item quantity="one">%1$s was removed from the group.</item>
<item quantity="other">%1$s were removed from the group.</item>
</plurals>
<string name="GroupUtil_group_name_is_now">Group name is now \'%1$s\'.</string>
<string name="GroupUtil_you_were_removed_from_group">You were removed from the group.</string>
<!-- profile_group_share_view -->
<string name="profile_group_share_view__make_your_profile_name_and_photo_visible_to_this_group">Make your profile name and photo visible to this group?</string>

View File

@ -4,7 +4,7 @@
<domain includeSubdomains="true">imaginary.stream</domain>
<domain includeSubdomains="true">storage.seed1.loki.network</domain>
<domain includeSubdomains="true">storage.seed2.loki.network</domain>
<domain includeSubdomains="true">http://public.loki.foundation</domain>
<domain includeSubdomains="true">public.loki.foundation:22023</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@ -521,18 +521,18 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void createDefaultPublicChatsIfNeeded() {
List<LokiPublicChat> defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG);
for (LokiPublicChat publiChat : defaultPublicChats) {
long threadID = GroupManager.getThreadId(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);
}
}
@ -545,7 +545,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());
}
}
@ -554,7 +554,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

View File

@ -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<Recipient> members;
protected String name;
protected Set<Recipient> admins;
public SignalGroupTask(GroupCreateActivity activity,
Bitmap avatar,
String name,
Set<Recipient> members)
Set<Recipient> members,
Set<Recipient> 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<Recipient> members) {
super(activity, avatar, name, members);
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins) {
super(activity, avatar, name, members, admins);
}
@Override
protected Optional<GroupActionResult> 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<Recipient> members)
Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins)
{
super(activity, avatar, name, members);
super(activity, avatar, name, members, admins);
this.groupId = groupId;
}
@Override
protected Optional<GroupActionResult> 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();
}
@ -491,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())) {
@ -537,11 +543,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
existingContacts.addAll(recipients);
if (group.isPresent()) {
List<Address> adminList = group.get().getAdmins();
final Set<Recipient> 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<Recipient> admins;
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name, Set<Recipient> admins) {
this.id = id;
this.recipients = recipients;
this.avatarBmp = avatarBmp;
this.avatarBytes = avatarBytes;
this.name = name;
this.admins = admins;
}
}
}

View File

@ -740,9 +740,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);
@ -762,7 +762,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()) {
@ -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 {
@ -2077,7 +2081,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("Session Updates"))) {
if (recipient.isGroupRecipient() && recipient.getAddress().isRSSFeed()) {
unblockButton.setVisibility(View.GONE);
composePanel.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE);
@ -2106,7 +2110,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()) {
@ -2461,7 +2465,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)
@ -2521,7 +2525,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)

View File

@ -52,17 +52,9 @@ public class Address implements Parcelable, Comparable<Address> {
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<Address> {
return Address.fromSerialized(external);
}
public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) {
return new Address(serialized, true);
}
public static @NonNull List<Address> fromSerializedList(@NonNull String serialized, char delimiter) {
String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter);
List<Address> addresses = new LinkedList<>();
@ -121,13 +109,15 @@ public class Address implements Parcelable, Comparable<Address> {
}
}
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<Address> {
}
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");

View File

@ -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<String> 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<Address> members, boolean mms) {
public String getOrCreateGroupForMembers(List<Address> members, boolean mms, List<Address> 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 {
@ -150,14 +152,33 @@ 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;
}
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<Address> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins)
{
Collections.sort(members);
@ -179,6 +200,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 +285,17 @@ public class GroupDatabase extends Database {
});
}
public void updateAdmins(String groupId, List<Address> 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<Address> currentMembers = getCurrentMembers(groupId);
currentMembers.remove(source);
@ -351,7 +387,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 +412,11 @@ public class GroupDatabase extends Database {
private final boolean active;
private final boolean mms;
private final String url;
private final List<Address> 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 +432,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() {
@ -448,6 +489,14 @@ public class GroupDatabase extends Database {
return mms;
}
public boolean isPublicChat() { return Address.fromSerialized(id).isPublicChat(); }
public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); }
public boolean isSignalGroup() { return Address.fromSerialized(id).isSignalGroup(); }
public String getUrl() { return url; }
public List<Address> getAdmins() { return admins; }
}
}

View File

@ -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);

View File

@ -35,14 +35,17 @@ 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.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiUserDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
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;
@ -76,8 +79,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV3 = 24;
private static final int lokiV4 = 25;
private static final int lokiV5 = 26;
private static final int lokiV6 = 27;
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 int DATABASE_VERSION = lokiV6; // 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;
@ -529,6 +533,43 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
}
if (oldVersion < lokiV6) {
// 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 });
}
// Add admin field in groups
db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -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);
}
@ -50,11 +55,12 @@ public class GroupManager {
@NonNull Set<Recipient> members,
@Nullable Bitmap avatar,
@Nullable String name,
boolean mms)
boolean mms,
@NonNull Set<Recipient> 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,
@ -62,56 +68,90 @@ public class GroupManager {
@NonNull Set<Recipient> members,
@Nullable Bitmap avatar,
@Nullable String name,
boolean mms)
boolean mms,
@NonNull Set<Recipient> 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<Address> memberAddresses = getMemberAddresses(members);
final Set<Address> adminAddresses = getMemberAddresses(admins);
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null);
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) {
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);
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);
}
*/
}
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<Address> memberAddresses = new HashSet<>();
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>());
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,
@NonNull String groupId,
@NonNull Set<Recipient> members,
@Nullable Bitmap avatar,
@Nullable String name)
@Nullable String name,
@NonNull Set<Recipient> admins)
throws InvalidNumberException
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final Set<Address> memberAddresses = getMemberAddresses(members);
final Set<Address> 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);
@ -123,7 +163,8 @@ public class GroupManager {
@NonNull String groupId,
@NonNull Set<Address> members,
@Nullable String groupName,
@Nullable byte[] avatar)
@Nullable byte[] avatar,
@NonNull Set<Address> admins)
{
try {
Attachment avatarAttachment = null;
@ -131,15 +172,20 @@ public class GroupManager {
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
List<String> numbers = new LinkedList<>();
for (Address member : members) {
numbers.add(member.serialize());
}
List<String> 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();

View File

@ -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,14 +24,20 @@ 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;
import java.util.Collections;
import java.util.HashSet;
@ -38,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;
@ -58,7 +67,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<GroupRecord> record = database.getGroup(id);
if (record.isPresent() && group.getType() == Type.UPDATE) {
@ -81,12 +90,13 @@ 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);
SignalServiceAttachment avatar = group.getAvatar().orNull();
List<Address> members = group.getMembers().isPresent() ? new LinkedList<Address>() : null;
List<Address> admins = group.getAdmins().isPresent() ? new LinkedList<>() : null;
if (group.getMembers().isPresent()) {
for (String member : group.getMembers().get()) {
@ -94,8 +104,25 @@ public class GroupMessageProcessor {
}
}
// We should only create the group if we are part of the member list
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;
}
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);
if (group.getMembers().isPresent()) {
establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
}
return storeMessage(context, content, group, builder.build(), outgoing);
}
@ -108,7 +135,16 @@ public class GroupMessageProcessor {
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
String id = GroupUtil.getEncodedId(group);
// Only update group if admin sent the message
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
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;
}
}
Set<Address> recordMembers = new HashSet<>(groupRecord.getMembers());
Set<Address> messageMembers = new HashSet<>();
@ -141,7 +177,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()) {
@ -155,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);
}
@ -163,7 +205,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()));
@ -179,14 +224,15 @@ public class GroupMessageProcessor {
boolean outgoing)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
String id = GroupUtil.getEncodedId(group);
List<Address> members = record.getMembers();
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);
@ -204,14 +250,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);
@ -260,7 +306,38 @@ public class GroupMessageProcessor {
builder.addAllMembers(group.getMembers().get());
}
if (group.getAdmins().isPresent()) {
builder.addAllAdmins(group.getAdmins().get());
}
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;
}
}
private static void establishSessionsWithMembersIfNeeded(Context context, List<String> 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;
});
}
}
}

View File

@ -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<GroupRecord> record = database.getGroup(encodeId);
Optional<GroupRecord> 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<AvatarDownloadJob> {
@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));
}
}
}

View File

@ -9,9 +9,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
@ -67,11 +65,11 @@ public class DirectoryRefreshJob extends BaseJob {
public void onRun() throws IOException {
Log.i(TAG, "DirectoryRefreshJob.onRun()");
if (recipient == null) {
DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
} else {
DirectoryHelper.refreshDirectoryFor(context, recipient);
}
// if (recipient == null) {
// DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
// } else {
// DirectoryHelper.refreshDirectoryFor(context, recipient);
// }
}
@Override

View File

@ -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);

View File

@ -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<String> members = new LinkedList<>();
for (Address member : record.getMembers()) {

View File

@ -291,6 +291,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(content);
// Loki - Session requests
handleSessionRequestIfNeeded(content);
// Loki - Store pre key bundle
// We shouldn't store it if it's a pairing message
if (!content.getPairingAuthorisation().isPresent()) {
@ -335,7 +338,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
} else {
// Loki - Don'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())
@ -345,7 +349,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());
}
@ -601,9 +605,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,
@ -690,7 +696,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());
@ -728,7 +734,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());
}
@ -738,7 +744,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()) {
@ -845,8 +851,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> 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 = 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();
}
// 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);
@ -1030,8 +1044,16 @@ 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.isGroupUpdate() ? 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();
}
// Ignore messages from ourselves
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
content.getSenderDevice(),
message.getTimestamp(), body,
@ -1217,10 +1239,29 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@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 (content.isFriendRequest() || isGroupChatMessage(content)) { return; }
becomeFriendsWithContact(content.getSender(), true);
becomeFriendsWithContact(content.getSender(), true, false);
}
private void becomeFriendsWithContact(String pubKey, boolean syncContact) {
private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) {
if (content.isFriendRequest() && isSessionRequest(content)) {
// 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;
});
}
}
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;
@ -1228,6 +1269,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);
@ -1248,13 +1294,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!content.isFriendRequest() || message.isGroupUpdate()) { return; }
if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; }
// This handles the case where another user sends us a regular message without authorisation
Promise<Boolean, Exception> 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 {
@ -1561,10 +1607,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());
@ -1712,15 +1759,15 @@ 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);
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);
}
}
private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) {
if (message.getMessage().getGroupInfo().isPresent()) {
if (message.getMessage().isGroupMessage()) {
return getSyncMessageDestination(message);
} else {
return getPrimaryDeviceRecipient(message.getDestination().get());
@ -1728,15 +1775,15 @@ 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);
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);
}
}
private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) {
if (message.isGroupMessage()) {
return getMessageDestination(content, message);
} else {
return getPrimaryDeviceRecipient(content.getSender());
@ -1794,7 +1841,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return true;
} else if (conversation.isGroupRecipient()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
: Optional.absent();
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
@ -1830,8 +1877,12 @@ 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();
return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage();
}
private void resetRecipientToPush(@NonNull Recipient recipient) {

View File

@ -12,8 +12,10 @@ 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;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
@ -25,13 +27,17 @@ 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;
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.thoughtcrime.securesms.util.TextSecurePreferences;
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;
@ -46,10 +52,13 @@ 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;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@ -157,7 +166,28 @@ 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<SendMessageResult> results = deliver(message, target);
String localNumber = TextSecurePreferences.getLocalNumber(context);
// Only send messages to the contacts we have sessions with
List<Address> 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; }
// 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<Address> others = Stream.of(target).filter(t -> !validTargets.contains(t)).toList();
for (Address device : others) {
MessageSender.sendBackgroundSessionRequest(context, device.toPhoneString());
}
List<SendMessageResult> results = deliver(message, validTargets);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
Set<Address> successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet());
@ -231,7 +261,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<SendMessageResult> results = new ArrayList<>();
for (Address destination : destinations) results.add(SendMessageResult.networkFailure(new SignalServiceAddress(destination.toPhoneString())));
return results;
}
String groupId = groupAddress.toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<Quote> quote = getQuoteFor(message);
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
@ -247,24 +285,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, groupContext.getAdminsList());
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.withExpiration(message.getRecipient().getExpireMessages())
.withBody(message.getBody())
.asGroupMessage(group)
.build();
// Loki - Disable group updates for now
List<SendMessageResult> 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 +326,56 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
}
private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
ArrayList<Address> 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<Address> 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 {
/*
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<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
Set<Address> memberSet = new HashSet<>();
if (destinations.isEmpty()) {
List<Recipient> 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());
}
// 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<String> 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
}
}
return new LinkedList<>(memberSet);
}
return result;
/*
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList();
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
return Stream.of(members).map(Recipient::getAddress).toList();
*/
}
public static class Factory implements Job.Factory<PushGroupSendJob> {

View File

@ -97,15 +97,20 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType {
}
List<String> members = new LinkedList<>();
for (Address member : record.get().getMembers()) {
members.add(member.serialize());
}
List<String> admins = new LinkedList<>();
for (Address admin : record.get().getAdmins()) {
admins.add(admin.serialize());
}
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
.withAvatar(avatar)
.withId(groupId)
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.withMembers(members)
.withAdmins(admins)
.withName(record.get().getTitle())
.build();

View File

@ -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()

View File

@ -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);

View File

@ -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 }

View File

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPublicChatPoller
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<Long, LokiPublicChat>()
@ -50,10 +49,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)
@ -74,7 +73,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) {

View File

@ -45,6 +45,11 @@ 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)

View File

@ -33,6 +33,9 @@ data class BackgroundMessage private constructor(val data: Map<String, Any>) {
@JvmStatic
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "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<String, Any> ?: throw AssertionError("JSON parsing failed")
@ -99,6 +102,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 {

View File

@ -0,0 +1,152 @@
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
import android.support.v7.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
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<List<String>> {
private var members = listOf<String>()
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<String>
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<List<String>> {
return CreateClosedGroupLoader(this)
}
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
update(members)
}
override fun onLoaderReset(loader: Loader<List<String>>) {
update(listOf())
}
private fun update(members: List<String>) {
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 = nameEditText.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() < 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 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)
}
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<CreateClosedGroupActivity>,
private val avatar: Bitmap?,
private val name: String?,
private val members: Set<Recipient>,
private val admins: Set<Recipient>
) : AsyncTask<Void, Void, Optional<GroupManager.GroupActionResult>>() {
override fun doInBackground(vararg params: Void?): Optional<GroupManager.GroupActionResult> {
val activity = activity.get() ?: return Optional.absent()
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins))
}
override fun onPostExecute(result: Optional<GroupManager.GroupActionResult>) {
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()
}
}
}
// endregion
}

View File

@ -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<CreateClosedGroupAdapter.ViewHolder>() {
lateinit var glide: GlideRequests
val selectedMembers = mutableSetOf<String>()
var members = listOf<String>()
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)
}

View File

@ -0,0 +1,33 @@
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<List<String>>(context) {
override fun loadInBackground(): List<String> {
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<String>()
while (reader.next != null) {
val thread = reader.current
if (thread.recipient.isGroupRecipient) { continue }
if (lokiThreadDatabase.getFriendRequestStatus(thread.threadId) != LokiThreadFriendRequestStatus.FRIENDS) { continue }
val hexEncodedPublicKey = thread.recipient.address.toString().toLowerCase()
if (userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey)) { continue }
result.add(hexEncodedPublicKey)
}
return result
}
}

View File

@ -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)

View File

@ -111,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, 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 {

View File

@ -63,7 +63,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, null)
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false)
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())

View File

@ -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
}

View File

@ -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));

View File

@ -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;
}

View File

@ -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,
@ -202,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) {
if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
}
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
@ -215,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) {
if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
}
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());

View File

@ -11,11 +11,13 @@ import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.Address;
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;
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 +30,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 +70,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<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
String encodedGroupId = groupRecipient.getAddress().toGroupString();
@ -88,7 +118,7 @@ public class GroupUtil {
}
try {
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
return new GroupDescription(context, groupContext);
} catch (IOException e) {
Log.w(TAG, e);
@ -100,24 +130,49 @@ public class GroupUtil {
@NonNull private final Context context;
@Nullable private final GroupContext groupContext;
@Nullable private final List<Recipient> members;
private final List<Recipient> members;
private final List<Recipient> removedMembers;
private boolean ourDeviceWasRemoved;
public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
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<String> memberList = groupContext.getMembersList();
List<Address> currentMembers = getCurrentGroupMembers();
// 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()));
@ -127,14 +182,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));
}
@ -143,7 +204,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);
}
@ -162,5 +223,23 @@ public class GroupUtil {
return result;
}
private List<Address> 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;
}
}
}

View File

@ -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);