diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixChannel.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixChannel.java index 34d586d84..0b7d7896f 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixChannel.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixChannel.java @@ -2,4 +2,8 @@ package org.jivesoftware.smackx.mix.core; public class MixChannel { + + public MixChannel(/*EntityBareJid mixChannelAddress, DiscoverInfo channelInfo*/) { + //this.identityName = channelInfo.getIdentities("conference", "mix").get(0).getName(); + } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixManager.java index 569ef4a9a..d57f75053 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixManager.java @@ -16,18 +16,39 @@ */ package org.jivesoftware.smackx.mix.core; +import static org.jivesoftware.smackx.mix.core.MixCoreConstants.FEATURE_CORE_1; + +import java.util.List; import java.util.Map; import java.util.WeakHashMap; +import java.util.stream.Collectors; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; +import org.jivesoftware.smackx.disco.packet.DiscoverItems; +import org.jivesoftware.smackx.mam.MamManager; +import org.jivesoftware.smackx.mam.element.MamElements; +import org.jivesoftware.smackx.mix.core.exception.NotAMixChannelOrNoPermissionToSubscribeException; +import org.jivesoftware.smackx.mix.core.exception.NotAMixServiceException; +import org.jivesoftware.smackx.pubsub.packet.PubSub; +import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; + +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.Jid; +import org.jxmpp.util.cache.ExpirationCache; public final class MixManager extends Manager { private static final Map INSTANCES = new WeakHashMap<>(); + private static final ExpirationCache KNOWN_MIX_SERVICES = + new ExpirationCache<>(100, 1000 * 60 * 60 * 24); static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @@ -38,6 +59,8 @@ public final class MixManager extends Manager { }); } + private final ServiceDiscoveryManager serviceDiscoveryManager; + public static MixManager getInstanceFor(XMPPConnection connection) { MixManager manager = INSTANCES.get(connection); if (manager == null) { @@ -49,6 +72,107 @@ public final class MixManager extends Manager { private MixManager(XMPPConnection connection) { super(connection); - ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MixCoreConstants.FEATURE_CORE_1); + serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection()); + serviceDiscoveryManager.addFeature(FEATURE_CORE_1); + } + + public List discoverMixChannels(DomainBareJid mixServiceAddress) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, NotAMixServiceException, + SmackException.FeatureNotSupportedException { + MixService mixService = getOrDiscoverMixService(mixServiceAddress); + + if (!mixService.isSearchable()) { + throw new SmackException.FeatureNotSupportedException(MixCoreConstants.FEATURE_SEARCHABLE_1, mixServiceAddress); + } + + DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(mixServiceAddress); + List channelJids = discoverItems.getItems().stream() + .map(DiscoverItems.Item::getEntityID) + .map(Jid::asEntityBareJidOrThrow) + .collect(Collectors.toList()); + return channelJids; + } + + private MixService getOrDiscoverMixService(DomainBareJid mixServiceAddress) + throws SmackException.NoResponseException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, NotAMixServiceException { + if (KNOWN_MIX_SERVICES.containsKey(mixServiceAddress)) { + return KNOWN_MIX_SERVICES.get(mixServiceAddress); + } + return discoverMixService(mixServiceAddress); + } + + private MixService discoverMixService(DomainBareJid mixServiceAddress) + throws SmackException.NoResponseException, XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, NotAMixServiceException { + DiscoverInfo mixServiceDiscoverInfo = serviceDiscoveryManager.discoverInfo(mixServiceAddress); + + if (!mixServiceDiscoverInfo.hasIdentity("conference", "mix")) { + throw new NotAMixServiceException("Identity of the service MUST have a category 'conference' and a type of 'mix'."); + } + if (!mixServiceDiscoverInfo.containsFeature(FEATURE_CORE_1)) { + throw new NotAMixServiceException("Service MUST include the '" + FEATURE_CORE_1 + "' feature."); + } + + mixServiceStandardComplianceChecks(mixServiceDiscoverInfo); + + MixService mixService = new MixService(mixServiceAddress, mixServiceDiscoverInfo); + + KNOWN_MIX_SERVICES.put(mixServiceAddress, mixService); + + return mixService; + } + + public MixChannel discoverMixChannelInformation(EntityBareJid mixChannelAddress) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException, NotAMixChannelOrNoPermissionToSubscribeException { + DiscoverInfo channelInfo = serviceDiscoveryManager.discoverInfo(mixChannelAddress); + + if (!channelInfo.hasIdentity("conference", "mix")) { + throw new NotAMixChannelOrNoPermissionToSubscribeException( + "DiscoverInfo did not contain identity with category 'conference' and type 'mix'."); + } + + mixChannelStandardComplianceChecks(channelInfo); + + //return new MixChannel(mixChannelAddress, channelInfo); + return null; + } + + /** + * Perform checks for standards compliance on the service. + * These checks are not strictly necessary, but may be used to identify faulty server implementations. + * + * @param info DiscoverInfo of the mix service. + */ + private void mixServiceStandardComplianceChecks(DiscoverInfo info) { + if (info.containsFeature(MamElements.NAMESPACE)) { + // XEP-0369: §6.1 + throw new AssertionError("A MIX service MUST NOT advertise support for MAM."); + } + if (info.containsFeature(PubSub.NAMESPACE)) { + // XEP-0369: §6.1 + throw new AssertionError("A MIX service MUST NOT advertise support for PubSub."); + } + } + + private void mixChannelStandardComplianceChecks(DiscoverInfo channelInfo) throws NotAMixChannelOrNoPermissionToSubscribeException { + if (!channelInfo.containsFeature(FEATURE_CORE_1)) { + throw new NotAMixChannelOrNoPermissionToSubscribeException( + "MIX channel did not advertise feature '" + FEATURE_CORE_1 + "'."); + } + if (!channelInfo.containsFeature(MamElements.NAMESPACE)) { + throw new NotAMixChannelOrNoPermissionToSubscribeException( + "MIX channel did not advertise feature '" + MamElements.NAMESPACE + "'."); + } + } + + public MixChannel join(EntityBareJid channelAddress, String nick) + throws XMPPException.XMPPErrorException, NotAMixChannelOrNoPermissionToSubscribeException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + MixChannel channel = discoverMixChannelInformation(channelAddress); + + return null; } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixNodes.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixNodes.java index 30c5819da..ca95a5c31 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixNodes.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixNodes.java @@ -17,10 +17,11 @@ package org.jivesoftware.smackx.mix.core; import org.jivesoftware.smackx.mix.core.element.SubscribeElement; +import org.jivesoftware.smackx.mix.presence.MixPresenceConstants; public class MixNodes { public static final SubscribeElement NODE_MESSAGES = new SubscribeElement(MixCoreConstants.NODE_MESSAGES); - public static final SubscribeElement NODE_PRESENCE = new SubscribeElement(MixCoreConstants.NODE_PRESENCE); + public static final SubscribeElement NODE_PRESENCE = new SubscribeElement(MixPresenceConstants.NODE_PRESENCE); public static final SubscribeElement NODE_PARTICIPANTS = new SubscribeElement(MixCoreConstants.NODE_PARTICIPANTS); public static final SubscribeElement NODE_INFO = new SubscribeElement(MixCoreConstants.NODE_INFO); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixService.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixService.java new file mode 100644 index 000000000..1c969b5df --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/MixService.java @@ -0,0 +1,37 @@ +package org.jivesoftware.smackx.mix.core; + +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; + +import org.jxmpp.jid.DomainBareJid; + +public class MixService { + + private final DomainBareJid serviceJid; + + private boolean isSearchable; + + private boolean isSupportingChannelCreation; + + public MixService(DomainBareJid serviceJid, DiscoverInfo serviceInfo) { + this.serviceJid = serviceJid; + + this.isSearchable = discoverIsSearchable(serviceInfo); + this.isSupportingChannelCreation = discoverIsSupportingChannelCreation(serviceInfo); + } + + public boolean isSearchable() { + return isSearchable; + } + + public boolean isSupportingChannelCreation() { + return isSupportingChannelCreation; + } + + private boolean discoverIsSupportingChannelCreation(DiscoverInfo serviceInfo) { + return serviceInfo.containsFeature(MixCoreConstants.FEATURE_CREATE_CHANNEL_1); + } + + private boolean discoverIsSearchable(DiscoverInfo serviceInfo) { + return serviceInfo.containsFeature(MixCoreConstants.FEATURE_SEARCHABLE_1); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixChannelOrNoPermissionToSubscribeException.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixChannelOrNoPermissionToSubscribeException.java new file mode 100644 index 000000000..3b3947484 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixChannelOrNoPermissionToSubscribeException.java @@ -0,0 +1,10 @@ +package org.jivesoftware.smackx.mix.core.exception; + +public class NotAMixChannelOrNoPermissionToSubscribeException extends Exception { + + private static final long serialVersionUID = 1L; + + public NotAMixChannelOrNoPermissionToSubscribeException(String message) { + super(message); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixServiceException.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixServiceException.java new file mode 100644 index 000000000..9cfa7f716 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/core/exception/NotAMixServiceException.java @@ -0,0 +1,10 @@ +package org.jivesoftware.smackx.mix.core.exception; + +public class NotAMixServiceException extends Exception { + + private static final long serialVersionUID = 1L; + + public NotAMixServiceException(String message) { + super(message); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/MixMiscConstants.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/MixMiscConstants.java new file mode 100644 index 000000000..30ed60fe2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/MixMiscConstants.java @@ -0,0 +1,16 @@ +package org.jivesoftware.smackx.mix.misc; + +import static org.jivesoftware.smackx.mix.core.MixCoreConstants.URN_XMPP_MIX; + +public class MixMiscConstants { + + public static final String URN_XMPP_MIX_MISC = URN_XMPP_MIX + ":misc"; + + public static final String URN_XMPP_MIX_MISC_0 = URN_XMPP_MIX_MISC + ":0"; + + public static final String NAMESPACE_MISC_0 = URN_XMPP_MIX_MISC_0; + + public static final String NAMESPACE_MISC = NAMESPACE_MISC_0; + + public static final String FEATURE_NICK_REGISTER_0 = NAMESPACE_MISC_0 + "#nick-register"; +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/element/RegisterElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/element/RegisterElement.java new file mode 100644 index 000000000..dbd0028c2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/element/RegisterElement.java @@ -0,0 +1,45 @@ +package org.jivesoftware.smackx.mix.misc.element; + +import static org.jivesoftware.smackx.mix.misc.MixMiscConstants.NAMESPACE_MISC_0; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smackx.mix.core.element.NickElement; + +public abstract class RegisterElement implements ExtensionElement { + + public static final String ELEMENT = "register"; + + private final NickElement nick; + + public RegisterElement(NickElement nickElement) { + this.nick = Objects.requireNonNull(nickElement, "Nick element MUST NOT be null."); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + return new XmlStringBuilder(this) + .rightAngleBracket() + .append(nick) + .closeElement(this); + } + + public static class V0 extends RegisterElement { + + public V0(NickElement nickElement) { + super(nickElement); + } + + @Override + public String getNamespace() { + return NAMESPACE_MISC_0; + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/provider/RegisterElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/provider/RegisterElementProvider.java new file mode 100644 index 000000000..196cb6fe3 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mix/misc/provider/RegisterElementProvider.java @@ -0,0 +1,37 @@ +package org.jivesoftware.smackx.mix.misc.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.mix.core.element.NickElement; +import org.jivesoftware.smackx.mix.core.element.SetNickElement; +import org.jivesoftware.smackx.mix.misc.element.RegisterElement; + +public abstract class RegisterElementProvider extends ExtensionElementProvider { + + public static class V0 extends RegisterElementProvider { + + @Override + public RegisterElement.V0 parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) + throws XmlPullParserException, IOException, SmackParsingException { + NickElement nickElement = null; + while (true) { + XmlPullParser.TagEvent tagEvent = parser.nextTag(); + String name = parser.getName(); + if (tagEvent == XmlPullParser.TagEvent.START_ELEMENT) { + if (NickElement.ELEMENT.equals(name)) { + nickElement = new NickElement(parser.nextText()); + } + } else if (tagEvent == XmlPullParser.TagEvent.END_ELEMENT) { + if (SetNickElement.ELEMENT.equals(name)) { + return new RegisterElement.V0(nickElement); + } + } + } + } + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mix/misc/element/RegisterElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mix/misc/element/RegisterElementTest.java new file mode 100644 index 000000000..c404e4eab --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mix/misc/element/RegisterElementTest.java @@ -0,0 +1,21 @@ +package org.jivesoftware.smackx.mix.misc.element; + +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; + +import org.jivesoftware.smackx.mix.core.element.NickElement; + +import org.junit.jupiter.api.Test; + +public class RegisterElementTest { + + @Test + public void v0testSerialization() { + RegisterElement register = new RegisterElement.V0(new NickElement("thirdwitch")); + String expectedXml = "" + + "\n" + + " thirdwitch\n" + + "\n"; + + assertXmlSimilar(expectedXml, register.toXML()); + } +}