From ce19ea41140fa191a52e3b94c17b54ebee9d2603 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Feb 2018 08:51:54 +0100 Subject: [PATCH] Add support for XEP-0382: Spoiler Messages Fixes SMACK-795. --- documentation/extensions/index.md | 1 + documentation/extensions/spoiler.md | 33 ++++ .../smackx/spoiler/SpoilerManager.java | 69 ++++++++ .../spoiler/element/SpoilerElement.java | 165 ++++++++++++++++++ .../smackx/spoiler/element/package-info.java | 20 +++ .../smackx/spoiler/package-info.java | 20 +++ .../spoiler/provider/SpoilerProvider.java | 49 ++++++ .../smackx/spoiler/provider/package-info.java | 20 +++ .../experimental.providers | 7 + .../smackx/spoiler/SpoilerTest.java | 126 +++++++++++++ .../smackx/spoiler/package-info.java | 20 +++ 11 files changed, 530 insertions(+) create mode 100644 documentation/extensions/spoiler.md create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/SpoilerProvider.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/package-info.java create mode 100644 smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java create mode 100644 smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/package-info.java diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 46470b84d..6e737430a 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -94,6 +94,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. | | Stable and Unique Stanza IDs | [XEP-0359](http://xmpp.org/extensions/xep-0359.html) | This specification describes unique and stable IDs for messages. | | HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. | +| [Spoiler Messages](spoiler.md) | [XEP-0382](http://xmpp.org/extensions/xep-0382.html) | Indicate that the body of a message should be treated as a spoiler | | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. | | [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-XXXX](https://conversations.im/omemo/xep-omemo.html) | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). | | [Consistent Color Generation](consistent_colors.md) | [XEP-0392](http://xmpp.org/extensions/xep-0392.html) | Generate consistent colors for identifiers like usernames to provide a consistent user experience. | diff --git a/documentation/extensions/spoiler.md b/documentation/extensions/spoiler.md new file mode 100644 index 000000000..5d4ac94ea --- /dev/null +++ b/documentation/extensions/spoiler.md @@ -0,0 +1,33 @@ +Spoiler Messages +================ + +[Back](index.md) + +Spoiler Messages can be used to indicate that the body of a message is a spoiler and should be displayed as such. + +## Usage + +To get an instance of the SpoilerManager, call +``` +SpoilerManager manager = SpoilerManager.getInstanceFor(connection); +``` +This will automatically add Spoilers to the list of supported features of your client. + +The manager can then be used to add SpoilerElements to messages like follows: +``` +Message message = new Message(); + +// spoiler without hint +SpoilerElement.addSpoiler(message); + +// spoiler with hint about content +SpoilerElement.addSpoiler(message, "End of Love Story"); + +// spoiler with localized hint +SpoilerElement.addSpoiler(message, "de", "Der Kuchen ist eine Lüge"); +``` + +To get Spoilers from a message call +``` +Map spoilers = SpoilerElement.getSpoilers(message); +``` \ No newline at end of file diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java new file mode 100644 index 000000000..1faf91a63 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java @@ -0,0 +1,69 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.spoiler; + +import java.util.Map; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; + +public final class SpoilerManager extends Manager { + + public static final String NAMESPACE_0 = "urn:xmpp:spoiler:0"; + + private static final Map INSTANCES = new WeakHashMap<>(); + + /** + * Create a new SpoilerManager and add Spoiler to disco features. + * + * @param connection xmpp connection + */ + private SpoilerManager(XMPPConnection connection) { + super(connection); + } + + /** + * Begin announcing support for Spoiler messages. + */ + public void startAnnounceSupport() { + ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(NAMESPACE_0); + } + + /** + * End announcing support for Spoiler messages. + */ + public void stopAnnounceSupport() { + ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(NAMESPACE_0); + } + + /** + * Return the connections instance of the SpoilerManager. + * + * @param connection xmpp connection + * @return SpoilerManager + */ + public static SpoilerManager getInstanceFor(XMPPConnection connection) { + SpoilerManager manager = INSTANCES.get(connection); + if (manager == null) { + manager = new SpoilerManager(connection); + INSTANCES.put(connection, manager); + } + return manager; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java new file mode 100644 index 000000000..1392cd65d --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/SpoilerElement.java @@ -0,0 +1,165 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.spoiler.element; + +import static org.jivesoftware.smackx.spoiler.SpoilerManager.NAMESPACE_0; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class SpoilerElement implements ExtensionElement { + + public static final String ELEMENT = "spoiler"; + public static final SpoilerElement EMPTY = new SpoilerElement(null, null); + + private final String hint; + private final String language; + + /** + * Create a new SpoilerElement with a hint about a content and a language attribute. + * + * @param language language of the hint. + * @param hint hint about the content. + */ + public SpoilerElement(String language, String hint) { + if (language != null && !language.equals("")) { + if (hint == null || hint.equals("")) { + throw new IllegalArgumentException("Hint cannot be null or empty if language is not empty."); + } + } + this.language = language; + this.hint = hint; + } + + /** + * Return the hint text of the spoiler. + * May be null. + * + * @return hint text + */ + public String getHint() { + return hint; + } + + /** + * Add a SpoilerElement to a message. + * + * @param message message to add the Spoiler to. + */ + public static void addSpoiler(Message message) { + message.addExtension(SpoilerElement.EMPTY); + } + + /** + * Add a SpoilerElement with a hint to a message. + * + * @param message Message to add the Spoiler to. + * @param hint Hint about the Spoilers content. + */ + public static void addSpoiler(Message message, String hint) { + message.addExtension(new SpoilerElement(null, hint)); + } + + /** + * Add a SpoilerElement with a hint in a certain language to a message. + * + * @param message Message to add the Spoiler to. + * @param lang language of the Spoiler hint. + * @param hint hint. + */ + public static void addSpoiler(Message message, String lang, String hint) { + message.addExtension(new SpoilerElement(lang, hint)); + } + + + /** + * Returns true, if the message has at least one spoiler element. + * + * @param message message + * @return true if message has spoiler extension + */ + public static boolean containsSpoiler(Message message) { + return message.hasExtension(SpoilerElement.ELEMENT, NAMESPACE_0); + } + + /** + * Return a map of all spoilers contained in a message. + * The map uses the language of a spoiler as key. + * If a spoiler has no language attribute, its key will be an empty String. + * + * @param message message + * @return map of spoilers + */ + public static Map getSpoilers(Message message) { + if (!containsSpoiler(message)) { + return null; + } + + List spoilers = message.getExtensions(SpoilerElement.ELEMENT, NAMESPACE_0); + Map map = new HashMap<>(); + + for (ExtensionElement e : spoilers) { + SpoilerElement s = (SpoilerElement) e; + if (s.getLanguage() == null || s.getLanguage().equals("")) { + map.put("", s.getHint()); + } else { + map.put(s.getLanguage(), s.getHint()); + } + } + + return map; + } + + /** + * Return the language of the hint. + * May be null. + * + * @return language of hint text + */ + public String getLanguage() { + return language; + } + + @Override + public String getNamespace() { + return NAMESPACE_0; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.optXmlLangAttribute(getLanguage()); + if (getHint() == null) { + xml.closeEmptyElement(); + } else { + xml.rightAngleBracket(); + xml.append(getHint()); + xml.closeElement(this); + } + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/package-info.java new file mode 100644 index 000000000..67e8b1307 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/element/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Smack's API for XEP-0382: Spoiler Messages. + */ +package org.jivesoftware.smackx.spoiler.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/package-info.java new file mode 100644 index 000000000..a7e811501 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Smack's API for XEP-0382: Spoiler Messages. + */ +package org.jivesoftware.smackx.spoiler; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/SpoilerProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/SpoilerProvider.java new file mode 100644 index 000000000..090d8c499 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/SpoilerProvider.java @@ -0,0 +1,49 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.spoiler.provider; + +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.spoiler.element.SpoilerElement; + +import org.xmlpull.v1.XmlPullParser; + +public class SpoilerProvider extends ExtensionElementProvider { + + public static SpoilerProvider INSTANCE = new SpoilerProvider(); + + @Override + public SpoilerElement parse(XmlPullParser parser, int initialDepth) throws Exception { + String lang = ParserUtils.getXmlLang(parser); + String hint = null; + + outerloop: while (true) { + int tag = parser.next(); + switch (tag) { + case TEXT: + hint = parser.getText(); + break; + case END_TAG: + break outerloop; + } + } + return new SpoilerElement(lang, hint); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/package-info.java new file mode 100644 index 000000000..5640cce8a --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/provider/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Smack's API for XEP-0382: Spoiler Messages. + */ +package org.jivesoftware.smackx.spoiler.provider; diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 32452dfb9..686db8ee6 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -296,6 +296,13 @@ org.jivesoftware.smackx.eme.provider.ExplicitMessageEncryptionProvider + + + spoiler + urn:xmpp:spoiler:0 + org.jivesoftware.smackx.spoiler.provider.SpoilerProvider + + markup diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java new file mode 100644 index 000000000..3049e44df --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java @@ -0,0 +1,126 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.spoiler; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +import java.util.Map; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smackx.spoiler.element.SpoilerElement; +import org.jivesoftware.smackx.spoiler.provider.SpoilerProvider; + +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; + +public class SpoilerTest extends SmackTestSuite { + + @Test + public void emptySpoilerTest() throws Exception { + final String xml = ""; + + Message message = new Message(); + SpoilerElement.addSpoiler(message); + + SpoilerElement empty = message.getExtension(SpoilerElement.ELEMENT, SpoilerManager.NAMESPACE_0); + + assertNull(empty.getHint()); + assertNull(empty.getLanguage()); + + assertXMLEqual(xml, empty.toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + SpoilerElement parsed = SpoilerProvider.INSTANCE.parse(parser); + assertXMLEqual(xml, parsed.toXML().toString()); + } + + @Test + public void hintSpoilerTest() throws Exception { + final String xml = "Love story end"; + + Message message = new Message(); + SpoilerElement.addSpoiler(message, "Love story end"); + + SpoilerElement withHint = message.getExtension(SpoilerElement.ELEMENT, SpoilerManager.NAMESPACE_0); + + assertEquals("Love story end", withHint.getHint()); + assertNull(withHint.getLanguage()); + + assertXMLEqual(xml, withHint.toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + SpoilerElement parsed = SpoilerProvider.INSTANCE.parse(parser); + + assertXMLEqual(xml, parsed.toXML().toString()); + } + + @Test + public void i18nHintSpoilerTest() throws Exception { + final String xml = "Der Kuchen ist eine Lüge!"; + + Message message = new Message(); + SpoilerElement.addSpoiler(message, "de", "Der Kuchen ist eine Lüge!"); + + SpoilerElement i18nHint = message.getExtension(SpoilerElement.ELEMENT, SpoilerManager.NAMESPACE_0); + + assertEquals("Der Kuchen ist eine Lüge!", i18nHint.getHint()); + assertEquals("de", i18nHint.getLanguage()); + + assertXMLEqual(xml, i18nHint.toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + SpoilerElement parsed = SpoilerProvider.INSTANCE.parse(parser); + assertEquals(i18nHint.getLanguage(), parsed.getLanguage()); + + assertXMLEqual(xml, parsed.toXML().toString()); + } + + @Test + public void getSpoilersTest() { + Message m = new Message(); + assertNull(SpoilerElement.getSpoilers(m)); + + SpoilerElement.addSpoiler(m); + assertTrue(SpoilerElement.containsSpoiler(m)); + + Map spoilers = SpoilerElement.getSpoilers(m); + assertEquals(1, spoilers.size()); + assertEquals(null, spoilers.get("")); + + final String spoilerText = "Spoiler Text"; + + SpoilerElement.addSpoiler(m, "de", spoilerText); + spoilers = SpoilerElement.getSpoilers(m); + assertEquals(2, spoilers.size()); + assertEquals(spoilerText, spoilers.get("de")); + } + + @Test(expected = IllegalArgumentException.class) + public void spoilerCheckArgumentsNullTest() { + SpoilerElement spoilerElement = new SpoilerElement("de", null); + } + + @Test(expected = IllegalArgumentException.class) + public void spoilerCheckArgumentsEmptyTest() { + SpoilerElement spoilerElement = new SpoilerElement("de", ""); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/package-info.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/package-info.java new file mode 100644 index 000000000..a7e811501 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Smack's API for XEP-0382: Spoiler Messages. + */ +package org.jivesoftware.smackx.spoiler;