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 contactAddress = BehaviorSubject.create(); private BehaviorSubject contactDisplayName = BehaviorSubject.create(); private BehaviorSubject contactAccountAddress = BehaviorSubject.create(); private BehaviorSubject contactPresenceMode = BehaviorSubject.create(); private BehaviorSubject contactPresenceStatus = BehaviorSubject.create(); private BehaviorSubject> contactGroups = BehaviorSubject.create(); private BehaviorSubject> contactIdentityFingerprint = BehaviorSubject.createDefault(new Optional<>()); private BehaviorSubject> contactDeviceFingerprints = BehaviorSubject.createDefault(Collections.emptyList()); private BehaviorSubject> 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 groups = entry.getGroups(); List 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 getContactAddress() { return contactAddress; } public Observable getContactDisplayName() { return contactDisplayName; } public Observable getContactAccountAddress() { return contactAccountAddress; } public Observable getContactPresenceMode() { return contactPresenceMode; } public Observable getContactPresenceStatus() { return contactPresenceStatus; } public Observable> getContactGroups() { return contactGroups; } public Observable> getContactIdentityFingerprint() { return contactIdentityFingerprint; } public Observable> getContactDeviceFingerprints() { return contactDeviceFingerprints; } public Observable> getContactAvatarBase() { return contactAvatarBase; } public Single 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); } }