diff --git a/app/src/main/java/im/conversations/android/database/dao/PresenceDao.java b/app/src/main/java/im/conversations/android/database/dao/PresenceDao.java index 4ba615147..55a04f90f 100644 --- a/app/src/main/java/im/conversations/android/database/dao/PresenceDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/PresenceDao.java @@ -10,6 +10,7 @@ import im.conversations.android.database.entity.PresenceEntity; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.PresenceShow; import im.conversations.android.database.model.PresenceType; +import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import java.util.Arrays; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.parts.Resourcepart; @@ -37,7 +38,10 @@ public abstract class PresenceDao { @NonNull final Resourcepart resource, @Nullable final PresenceType type, @Nullable final PresenceShow show, - @Nullable final String status) { + @Nullable final String status, + @Nullable final String vCardPhoto, + @Nullable final String occupantId, + @Nullable final MultiUserChat multiUserChat) { if (resource.equals(Resourcepart.EMPTY) && Arrays.asList(PresenceType.ERROR, PresenceType.UNAVAILABLE).contains(type)) { deletePresences(account.id, address); @@ -49,7 +53,17 @@ public abstract class PresenceDao { // unavailable presence only delete previous nothing left to do return; } - final var entity = PresenceEntity.of(account.id, address, resource, type, show, status); + final var entity = + PresenceEntity.of( + account.id, + address, + resource, + type, + show, + status, + vCardPhoto, + occupantId, + multiUserChat); insert(entity); } } diff --git a/app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java b/app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java index 2ec42f31b..79480f95f 100644 --- a/app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java +++ b/app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java @@ -10,6 +10,7 @@ import im.conversations.android.database.model.PresenceShow; import im.conversations.android.database.model.PresenceType; import im.conversations.android.xmpp.model.muc.Affiliation; import im.conversations.android.xmpp.model.muc.Role; +import im.conversations.android.xmpp.model.muc.user.MultiUserChat; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; @@ -70,9 +71,13 @@ public class PresenceEntity { long account, @NonNull BareJid address, @NonNull Resourcepart resource, - PresenceType type, - PresenceShow show, - String status) { + final PresenceType type, + final PresenceShow show, + final String status, + final String vCardPhoto, + final String occupantId, + final MultiUserChat multiUserChat) { + final var mucItem = multiUserChat == null ? null : multiUserChat.getItem(); final var entity = new PresenceEntity(); entity.accountId = account; entity.address = address; @@ -80,6 +85,14 @@ public class PresenceEntity { entity.type = type; entity.show = show; entity.status = status; + entity.vCardPhoto = vCardPhoto; + if (mucItem != null) { + entity.occupantId = occupantId; + entity.mucUserAffiliation = mucItem.getAffiliation(); + entity.mucUserRole = mucItem.getRole(); + entity.mucUserJid = mucItem.getJid(); + entity.mucUserSelf = multiUserChat.getStatus().contains(110); + } return entity; } } diff --git a/app/src/main/java/im/conversations/android/dns/Resolver.java b/app/src/main/java/im/conversations/android/dns/Resolver.java index 84417a408..74b24fcf3 100644 --- a/app/src/main/java/im/conversations/android/dns/Resolver.java +++ b/app/src/main/java/im/conversations/android/dns/Resolver.java @@ -229,7 +229,7 @@ public class Resolver { } private static List resolveNoSrvRecords(DNSName dnsName, boolean includeCName) { - List results = new ArrayList<>(); + var results = new ImmutableList.Builder(); try { for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) { results.add(ServiceRecord.createDefault(dnsName, a.getInetAddress())); @@ -238,17 +238,17 @@ public class Resolver { resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) { results.add(ServiceRecord.createDefault(dnsName, aaaa.getInetAddress())); } - if (results.size() == 0 && includeCName) { + if (results.build().isEmpty() && includeCName) { for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) { results.addAll(resolveNoSrvRecords(cname.name, false)); } } - } catch (Throwable throwable) { + } catch (final Throwable throwable) { LOGGER.info("Error resolving fallback records", throwable); } results.add(ServiceRecord.createDefault(dnsName)); - return results; + return results.build(); } private static ResolverResult resolveWithFallback( diff --git a/app/src/main/java/im/conversations/android/xml/Namespace.java b/app/src/main/java/im/conversations/android/xml/Namespace.java index 13ce1c23c..167654a57 100644 --- a/app/src/main/java/im/conversations/android/xml/Namespace.java +++ b/app/src/main/java/im/conversations/android/xml/Namespace.java @@ -99,5 +99,7 @@ public final class Namespace { public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; + public static final String VCARD_TEMP = "vcard-temp"; + public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update"; public static final String VERSION = "jabber:iq:version"; } diff --git a/app/src/main/java/im/conversations/android/xmpp/Managers.java b/app/src/main/java/im/conversations/android/xmpp/Managers.java index 6eeeb7839..90c57effd 100644 --- a/app/src/main/java/im/conversations/android/xmpp/Managers.java +++ b/app/src/main/java/im/conversations/android/xmpp/Managers.java @@ -15,6 +15,7 @@ import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.manager.ExternalDiscoManager; import im.conversations.android.xmpp.manager.HttpUploadManager; import im.conversations.android.xmpp.manager.JingleConnectionManager; +import im.conversations.android.xmpp.manager.MultiUserChatManager; import im.conversations.android.xmpp.manager.NickManager; import im.conversations.android.xmpp.manager.PepManager; import im.conversations.android.xmpp.manager.PresenceManager; @@ -45,6 +46,7 @@ public final class Managers { .put( JingleConnectionManager.class, new JingleConnectionManager(context, connection)) + .put(MultiUserChatManager.class, new MultiUserChatManager(context, connection)) .put(NickManager.class, new NickManager(context, connection)) .put(PepManager.class, new PepManager(context, connection)) .put(PresenceManager.class, new PresenceManager(context, connection)) diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java new file mode 100644 index 000000000..58d1ca293 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java @@ -0,0 +1,28 @@ +package im.conversations.android.xmpp.manager; + +import android.content.Context; +import im.conversations.android.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.muc.user.MultiUserChat; +import im.conversations.android.xmpp.model.stanza.Presence; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Resourcepart; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MultiUserChatManager extends AbstractManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(MultiUserChatManager.class); + + public MultiUserChatManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void enter(final BareJid room) { + final var presence = new Presence(); + presence.setTo(JidCreate.fullFrom(room, Resourcepart.fromOrThrowUnchecked("c3-test-user"))); + presence.addExtension(new MultiUserChat()); + LOGGER.info("sending {} ", presence); + connection.sendPresencePacket(presence); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java new file mode 100644 index 000000000..2f6663bdb --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java @@ -0,0 +1,65 @@ +package im.conversations.android.xmpp.model.muc.user; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; +import java.util.Locale; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@XmlElement +public class Item extends Extension { + + private static final Logger LOGGER = LoggerFactory.getLogger(Item.class); + + public Item() { + super(Item.class); + } + + public Affiliation getAffiliation() { + final var affiliation = this.getAttribute("affiliation"); + if (Strings.isNullOrEmpty(affiliation)) { + return Affiliation.NONE; + } + try { + return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + LOGGER.warn("could not parse affiliation {}", affiliation); + return Affiliation.NONE; + } + } + + public Role getRole() { + final var role = this.getAttribute("role"); + if (Strings.isNullOrEmpty(role)) { + return Role.NONE; + } + try { + return Role.valueOf(role.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + LOGGER.warn("could not parse role {}", role); + return Role.NONE; + } + } + + public String getNick() { + return this.getAttribute("nick"); + } + + public Jid getJid() { + final var jid = this.getAttribute("jid"); + if (Strings.isNullOrEmpty(jid)) { + return null; + } + try { + return JidCreate.from(jid); + } catch (final XmppStringprepException e) { + return null; + } + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java index f5a16d860..061e1deae 100644 --- a/app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java +++ b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java @@ -1,7 +1,10 @@ package im.conversations.android.xmpp.model.muc.user; +import com.google.common.collect.Collections2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; @XmlElement(name = "x") public class MultiUserChat extends Extension { @@ -9,4 +12,14 @@ public class MultiUserChat extends Extension { public MultiUserChat() { super(MultiUserChat.class); } + + public Item getItem() { + return this.getExtension(Item.class); + } + + public Collection getStatus() { + return Collections2.filter( + Collections2.transform(getExtensions(Status.class), Status::getCode), + Objects::nonNull); + } } diff --git a/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java new file mode 100644 index 000000000..0706585af --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.muc.user; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Status extends Extension { + + public Status() { + super(Status.class); + } + + public Integer getCode() { + return this.getOptionalIntAttribute("code").orNull(); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java new file mode 100644 index 000000000..9b424e3ee --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class VCard extends Extension { + + public VCard() { + super(VCard.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java new file mode 100644 index 000000000..1ba392b19 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP) +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlPackage; +import im.conversations.android.xml.Namespace; diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java new file mode 100644 index 000000000..cb1f86d05 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Photo extends Extension { + + public Photo() { + super(Photo.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java new file mode 100644 index 000000000..0be3f94b9 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class VCardUpdate extends Extension { + + public VCardUpdate() { + super(VCardUpdate.class); + } + + public Photo getPhoto() { + return this.getExtension(Photo.class); + } + + public String getHash() { + final var photo = getPhoto(); + return photo == null ? null : photo.getContent(); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java new file mode 100644 index 000000000..d6325c8cd --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP_UPDATE) +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlPackage; +import im.conversations.android.xml.Namespace; diff --git a/app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java index 09f120c5a..1f48a661a 100644 --- a/app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java +++ b/app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java @@ -3,10 +3,14 @@ package im.conversations.android.xmpp.processor; import android.content.Context; import im.conversations.android.database.model.PresenceShow; import im.conversations.android.database.model.PresenceType; +import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.muc.user.MultiUserChat; +import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.stanza.Presence; +import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,9 +42,35 @@ public class PresenceProcessor extends XmppConnection.Delegate implements Consum } final var show = PresenceShow.of(presencePacket.findChildContent("show")); final var status = presencePacket.findChildContent("status"); - getDatabase().presenceDao().set(getAccount(), address, resource, type, show, status); - // TODO store presence info (vCard + muc#user stuff + occupantId) + final var vCardUpdate = presencePacket.getExtension(VCardUpdate.class); + final var vCardPhoto = vCardUpdate == null ? null : vCardUpdate.getHash(); + final var muc = presencePacket.getExtension(MultiUserChat.class); + + final String occupantId; + if (muc != null && presencePacket.hasExtension(OccupantId.class)) { + if (getManager(DiscoManager.class) + .hasFeature(Entity.discoItem(address), Namespace.OCCUPANT_ID)) { + occupantId = presencePacket.getExtension(OccupantId.class).getId(); + } else { + occupantId = null; + } + } else { + occupantId = null; + } + + getDatabase() + .presenceDao() + .set( + getAccount(), + address, + resource, + type, + show, + status, + vCardPhoto, + occupantId, + muc); // TODO do this only for contacts? fetchCapabilities(presencePacket);