Add support for XEP-0382: Spoiler Messages

Fixes SMACK-795.
This commit is contained in:
Paul Schaub 2018-02-22 08:51:54 +01:00 committed by Florian Schmaus
parent a729a7c43b
commit ce19ea4114
11 changed files with 530 additions and 0 deletions

View File

@ -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. |

View File

@ -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<String, String> spoilers = SpoilerElement.getSpoilers(message);
```

View File

@ -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<XMPPConnection, SpoilerManager> 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;
}
}

View File

@ -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<String, String> getSpoilers(Message message) {
if (!containsSpoiler(message)) {
return null;
}
List<ExtensionElement> spoilers = message.getExtensions(SpoilerElement.ELEMENT, NAMESPACE_0);
Map<String, String> 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<SpoilerElement> {
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);
}
}

View File

@ -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;

View File

@ -296,6 +296,13 @@
<className>org.jivesoftware.smackx.eme.provider.ExplicitMessageEncryptionProvider</className>
</extensionProvider>
<!-- XEP-0382: Spoiler Messages -->
<extensionProvider>
<elementName>spoiler</elementName>
<namespace>urn:xmpp:spoiler:0</namespace>
<className>org.jivesoftware.smackx.spoiler.provider.SpoilerProvider</className>
</extensionProvider>
<!-- XEP-0394: Message Markup -->
<extensionProvider>
<elementName>markup</elementName>

View File

@ -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 = "<spoiler xmlns='urn:xmpp:spoiler:0'/>";
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 = "<spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>";
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 = "<spoiler xml:lang='de' xmlns='urn:xmpp:spoiler:0'>Der Kuchen ist eine Lüge!</spoiler>";
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<String, String> 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", "");
}
}

View File

@ -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;