Mercury-IM/domain/src/main/java/org/mercury_im/messenger/core/viewmodel/contact/ContactDetailViewModel.java

261 lines
10 KiB
Java

package org.mercury_im.messenger.core.viewmodel.contact;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterGroup;
import org.jivesoftware.smackx.ikey.util.IkeyTrust;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.DirectChatRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
import org.mercury_im.messenger.core.util.CombinedPresenceListener;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.util.Tuple;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.contact.Peer;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.subjects.BehaviorSubject;
import lombok.Getter;
public class ContactDetailViewModel implements MercuryViewModel {
private final MercuryConnectionManager connectionManager;
private final PeerRepository peerRepository;
private final DirectChatRepository directChatRepository;
private final OpenPgpRepository openPgpRepository;
private final IkeyRepository ikeyRepository;
private final SchedulersFacade schedulers;
private Roster roster;
private FilteredPresenceEventListener presenceEventListener;
private BehaviorSubject<EntityBareJid> contactAddress = BehaviorSubject.create();
private BehaviorSubject<String> contactDisplayName = BehaviorSubject.create();
private BehaviorSubject<EntityBareJid> contactAccountAddress = BehaviorSubject.create();
private BehaviorSubject<Presence.Mode> contactPresenceMode = BehaviorSubject.create();
private BehaviorSubject<String> contactPresenceStatus = BehaviorSubject.create();
private BehaviorSubject<List<String>> contactGroups = BehaviorSubject.create();
private BehaviorSubject<Optional<FingerprintViewItem>> contactIdentityFingerprint = BehaviorSubject.createDefault(new Optional<>());
private BehaviorSubject<List<FingerprintViewItem>> contactDeviceFingerprints = BehaviorSubject.createDefault(Collections.emptyList());
private BehaviorSubject<Tuple<String, EntityBareJid>> contactAvatarBase = BehaviorSubject.create();
@Getter
private UUID peerId;
@Getter
private UUID accountId;
@Inject
public ContactDetailViewModel(MercuryConnectionManager connectionManager,
PeerRepository peerRepository,
DirectChatRepository directChatRepository,
OpenPgpRepository openPgpRepository,
IkeyRepository ikeyRepository,
SchedulersFacade schedulers) {
this.connectionManager = connectionManager;
this.peerRepository = peerRepository;
this.directChatRepository = directChatRepository;
this.openPgpRepository = openPgpRepository;
this.ikeyRepository = ikeyRepository;
this.schedulers = schedulers;
}
public Completable init(UUID peerId) {
return peerRepository.getPeer(peerId)
.flatMapCompletable(this::init);
}
public Completable init(Peer peer) {
return Completable.fromAction(() -> {
this.peerId = peer.getId();
this.accountId = peer.getAccount().getId();
MercuryConnection connection = connectionManager.getConnection(peer.getAccount());
roster = Roster.getInstanceFor(connection.getConnection());
setupPresenceEventListener(roster, peer.getJid());
addDisposable(peerRepository
.observePeer(peerId)
.filter(Optional::isPresent)
.map(Optional::getItem)
.compose(schedulers.executeUiSafeObservable())
.subscribe(p -> {
contactAddress.onNext(peer.getJid());
contactDisplayName.onNext(peer.getDisplayName());
contactAccountAddress.onNext(peer.getAccount().getJid());
contactAvatarBase.onNext(new Tuple<>(peer.getDisplayName(), peer.getJid()));
}));
addDisposable(openPgpRepository
.observeFingerprints(peer.getAccount().getId(), peer.getJid())
.compose(schedulers.executeUiSafeObservable())
.subscribe(fingerprints -> contactDeviceFingerprints.onNext(fingerprints)));
addDisposable(ikeyRepository
.loadRecord(peer.getAccount().getId(), peer.getJid())
.compose(schedulers.executeUiSafeObservable())
.map(record -> new FingerprintViewItem(
peer.getAccount().getId(), record.getJid(),
new OpenPgpV4Fingerprint(record.getSuperordinate()),
record.getTimestamp(), record.getTimestamp(),
OpenPgpTrustStore.Trust.trusted) // TODO
)
.map(Optional::new)
.subscribe(contactIdentityFingerprint::onNext));
});
}
private void setupPresenceEventListener(Roster roster, EntityBareJid jid) {
if (presenceEventListener != null) {
if (presenceEventListener.getJid().equals(jid)) {
// already set up
return;
}
// outdated listener from previous contact
roster.removePresenceEventListener(presenceEventListener);
}
presenceEventListener = new FilteredPresenceEventListener(jid) {
@Override
public void presenceReceived(Presence presence) {
contactPresenceMode.onNext(presence.getMode());
if (presence.getStatus() != null) {
contactPresenceStatus.onNext(presence.getStatus());
}
RosterEntry entry = roster.getEntry(jid);
if (entry == null) {
return;
}
List<RosterGroup> groups = entry.getGroups();
List<String> groupNames = new ArrayList<>(groups.size());
for (RosterGroup group : groups) {
groupNames.add(group.getName());
}
contactGroups.onNext(groupNames);
}
};
// Trigger once
Presence presence = roster.getPresence(jid);
if (presence != null) {
presenceEventListener.presenceReceived(presence);
}
roster.addPresenceEventListener(presenceEventListener);
}
public Observable<EntityBareJid> getContactAddress() {
return contactAddress;
}
public Observable<String> getContactDisplayName() {
return contactDisplayName;
}
public Observable<EntityBareJid> getContactAccountAddress() {
return contactAccountAddress;
}
public Observable<Presence.Mode> getContactPresenceMode() {
return contactPresenceMode;
}
public Observable<String> getContactPresenceStatus() {
return contactPresenceStatus;
}
public Observable<List<String>> getContactGroups() {
return contactGroups;
}
public Observable<Optional<FingerprintViewItem>> getContactIdentityFingerprint() {
return contactIdentityFingerprint;
}
public Observable<List<FingerprintViewItem>> getContactDeviceFingerprints() {
return contactDeviceFingerprints;
}
public Observable<Tuple<String, EntityBareJid>> getContactAvatarBase() {
return contactAvatarBase;
}
public Single<UUID> getOrCreateChat() {
return peerRepository.getPeer(peerId)
.flatMapSingle(directChatRepository::getOrCreateChatWithPeer)
.map(Chat::getId);
}
public void changeContactName(String newName)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
if (!newName.trim().isEmpty()) {
RosterEntry entry = roster.getEntry(contactAddress.getValue());
entry.setName(newName);
}
}
public void markDeviceFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, boolean checked) {
openPgpRepository.storeTrust(accountId, contactAddress.getValue(), fingerprint,
checked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted)
.subscribe();
}
public void markIkeyFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, EntityBareJid owner, boolean isChecked) {
IkeyTrust trust = new IkeyTrust();
trust.setTrust(isChecked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted);
ikeyRepository.storeSuperordinateTrust(accountId, owner, fingerprint, trust)
.subscribe();
}
public static abstract class FilteredPresenceEventListener extends CombinedPresenceListener {
@Getter
private final EntityBareJid jid;
public FilteredPresenceEventListener(EntityBareJid jid) {
this.jid = jid;
}
@Override
public void presenceReceived(Jid address, Presence presence) {
EntityBareJid entityBareJid = address.asEntityBareJidIfPossible();
if (entityBareJid != null && entityBareJid.equals(jid)) {
presenceReceived(presence);
}
}
public abstract void presenceReceived(Presence presence);
}
@Override
public void dispose() {
compositeDisposable.dispose();
if (presenceEventListener != null) roster.removePresenceEventListener(presenceEventListener);
}
}