mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-27 06:22:07 +01:00
XEP-0084: User Avatars
Co-authored-by: vanitasvitae <vanitasvitae@fsfe.org>
This commit is contained in:
parent
9d626bf787
commit
cee80edb13
18 changed files with 1543 additions and 1 deletions
|
@ -49,6 +49,7 @@ Smack Extensions and currently supported XEPs of smack-extensions
|
|||
| Advanced Message Processing | [XEP-0079](https://xmpp.org/extensions/xep-0079.html) | n/a | Enables entities to request, and servers to perform, advanced processing of XMPP message stanzas. |
|
||||
| User Location | [XEP-0080](https://xmpp.org/extensions/xep-0080.html) | n/a | Enabled communicating information about the current geographical or physical location of an entity. |
|
||||
| XMPP Date Time Profiles | [XEP-0082](https://xmpp.org/extensions/xep-0082.html) | n/a | Standardization of Date and Time representation in XMPP. |
|
||||
| User Avatar | [XEP-0084](https://xmpp.org/extensions/xep-0084.html) | 1.1.2 | Allows to exchange user avatars, which are small images or icons associated with human users. |
|
||||
| Chat State Notifications | [XEP-0085](https://xmpp.org/extensions/xep-0085.html) | n/a | Communicating the status of a user in a chat session. |
|
||||
| [Time Exchange](time.md) | [XEP-0090](https://xmpp.org/extensions/xep-0090.html) | n/a | Allows local time information to be shared between users. |
|
||||
| Software Version | [XEP-0092](https://xmpp.org/extensions/xep-0092.html) | n/a | Retrieve and announce the software application of an XMPP entity. |
|
||||
|
@ -79,7 +80,6 @@ Smack Extensions and currently supported XEPs of smack-extensions
|
|||
| [Group Chat Invitations](invitation.md) | n/a | n/a | Send invitations to other users to join a group chat room. |
|
||||
| [Jive Properties](properties.md) | n/a | n/a | TODO |
|
||||
|
||||
|
||||
Experimental Smack Extensions and currently supported XEPs of smack-experimental
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.jivesoftware.smack.util;
|
|||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
@ -369,6 +370,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
|
|||
return this;
|
||||
}
|
||||
|
||||
public XmlStringBuilder optAttribute(String name, URL url) {
|
||||
if (url != null) {
|
||||
attribute(name, url.toExternalForm());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given attribute if {@code value => 0}.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2019 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.avatar;
|
||||
|
||||
import org.jxmpp.jid.EntityBareJid;
|
||||
|
||||
/**
|
||||
* The {@link AvatarMetadataStore} interface defines methods used by the {@link UserAvatarManager} to determine,
|
||||
* whether the client already has a local copy of a published avatar or if the user needs to be informed about the
|
||||
* update in order to download the image.
|
||||
*/
|
||||
public interface AvatarMetadataStore {
|
||||
|
||||
/**
|
||||
* Determine, if the client already has a copy of the avatar with {@code itemId} available or not.
|
||||
*
|
||||
* @param jid {@link EntityBareJid} of the entity that published the avatar.
|
||||
* @param itemId itemId of the avatar
|
||||
*
|
||||
* @return true if the client already has a local copy of the avatar, false otherwise
|
||||
*/
|
||||
boolean hasAvatarAvailable(EntityBareJid jid, String itemId);
|
||||
|
||||
/**
|
||||
* Mark the tuple (jid, itemId) as available. This means that the client already has a local copy of the avatar
|
||||
* available and wishes not to be notified about this particular avatar anymore.
|
||||
*
|
||||
* @param jid {@link EntityBareJid} of the entity that published the avatar.
|
||||
* @param itemId itemId of the avatar
|
||||
*/
|
||||
void setAvatarAvailable(EntityBareJid jid, String itemId);
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez, 2019 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.avatar;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import org.jivesoftware.smack.datatypes.UInt16;
|
||||
import org.jivesoftware.smack.datatypes.UInt32;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
|
||||
/**
|
||||
* User Avatar metadata info model class.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @author Paul Schaub
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public class MetadataInfo {
|
||||
|
||||
public static final int MAX_HEIGHT = 65536;
|
||||
public static final int MAX_WIDTH = 65536;
|
||||
|
||||
private final String id;
|
||||
private final URL url;
|
||||
private final UInt32 bytes;
|
||||
private final String type;
|
||||
private final UInt16 height;
|
||||
private final UInt16 width;
|
||||
|
||||
/**
|
||||
* MetadataInfo constructor.
|
||||
*
|
||||
* @param id SHA-1 hash of the image data
|
||||
* @param url http(s) url of the image
|
||||
* @param bytes size of the image in bytes
|
||||
* @param type content type of the image
|
||||
* @param pixelsHeight height of the image in pixels
|
||||
* @param pixelsWidth width of the image in pixels
|
||||
*/
|
||||
public MetadataInfo(String id, URL url, long bytes, String type, int pixelsHeight, int pixelsWidth) {
|
||||
this.id = StringUtils.requireNotNullNorEmpty(id, "ID is required.");
|
||||
this.url = url;
|
||||
this.bytes = UInt32.from(bytes);
|
||||
this.type = StringUtils.requireNotNullNorEmpty(type, "Content Type is required.");
|
||||
if (pixelsHeight < 0 || pixelsHeight > MAX_HEIGHT) {
|
||||
throw new IllegalArgumentException("Image height value must be between 0 and 65536.");
|
||||
}
|
||||
if (pixelsWidth < 0 || pixelsWidth > MAX_WIDTH) {
|
||||
throw new IllegalArgumentException("Image width value must be between 0 and 65536.");
|
||||
}
|
||||
this.height = UInt16.from(pixelsHeight);
|
||||
this.width = UInt16.from(pixelsWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id.
|
||||
*
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of the avatar image.
|
||||
*
|
||||
* @return the url
|
||||
*/
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of bytes.
|
||||
*
|
||||
* @return the amount of bytes
|
||||
*/
|
||||
public UInt32 getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type.
|
||||
*
|
||||
* @return the type
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height in pixels.
|
||||
*
|
||||
* @return the height in pixels
|
||||
*/
|
||||
public UInt16 getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width in pixels.
|
||||
*
|
||||
* @return the width in pixels
|
||||
*/
|
||||
public UInt16 getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.avatar;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
|
||||
/**
|
||||
* User Avatar metadata pointer model class.
|
||||
* A pointer element is used to point to an avatar which is not published via PubSub or HTTP, but provided by a
|
||||
* third-party service.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0084.html">XEP-0084: User Avatar</a>
|
||||
*/
|
||||
public class MetadataPointer {
|
||||
|
||||
private final String namespace;
|
||||
private final Map<String, Object> fields;
|
||||
|
||||
/**
|
||||
* Metadata Pointer constructor.
|
||||
*
|
||||
* The following example
|
||||
* <pre>
|
||||
* {@code
|
||||
* <pointer>
|
||||
* <x xmlns='http://example.com/virtualworlds'>
|
||||
* <game>Ancapistan</game>
|
||||
* <character>Kropotkin</character>
|
||||
* </x>
|
||||
* </pointer>
|
||||
* }
|
||||
* </pre>
|
||||
* can be created by constructing the object like this:
|
||||
* <pre>
|
||||
* {@code
|
||||
* Map fields = new HashMap<>();
|
||||
* fields.add("game", "Ancapistan");
|
||||
* fields.add("character", "Kropotkin");
|
||||
* MetadataPointer pointer = new MetadataPointer("http://example.com/virtualworlds", fields);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param namespace namespace of the child element of the metadata pointer.
|
||||
* @param fields fields of the child element as key, value pairs.
|
||||
*/
|
||||
public MetadataPointer(String namespace, Map<String, Object> fields) {
|
||||
this.namespace = StringUtils.requireNotNullNorEmpty(namespace, "Namespace MUST NOT be null, nor empty.");
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the pointers child element.
|
||||
*
|
||||
* @return the namespace
|
||||
*/
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields of the pointers child element.
|
||||
*
|
||||
* @return the fields
|
||||
*/
|
||||
public Map<String, Object> getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez, 2019 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.avatar;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import org.jivesoftware.smack.Manager;
|
||||
import org.jivesoftware.smack.SmackException.NoResponseException;
|
||||
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
||||
import org.jivesoftware.smack.XMPPConnection;
|
||||
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
|
||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||
import org.jivesoftware.smack.packet.Message;
|
||||
import org.jivesoftware.smack.util.SHA1;
|
||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
||||
import org.jivesoftware.smackx.avatar.element.DataExtension;
|
||||
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
|
||||
import org.jivesoftware.smackx.avatar.listener.AvatarListener;
|
||||
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
|
||||
import org.jivesoftware.smackx.pep.PepListener;
|
||||
import org.jivesoftware.smackx.pep.PepManager;
|
||||
import org.jivesoftware.smackx.pubsub.EventElement;
|
||||
import org.jivesoftware.smackx.pubsub.ItemsExtension;
|
||||
import org.jivesoftware.smackx.pubsub.LeafNode;
|
||||
import org.jivesoftware.smackx.pubsub.PayloadItem;
|
||||
import org.jivesoftware.smackx.pubsub.PubSubException;
|
||||
import org.jivesoftware.smackx.pubsub.PubSubManager;
|
||||
|
||||
import org.jxmpp.jid.EntityBareJid;
|
||||
|
||||
/**
|
||||
* User Avatar manager class.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @author Paul Schaub
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public final class UserAvatarManager extends Manager {
|
||||
|
||||
public static final String DATA_NAMESPACE = "urn:xmpp:avatar:data";
|
||||
public static final String METADATA_NAMESPACE = "urn:xmpp:avatar:metadata";
|
||||
public static final String FEATURE_METADATA = METADATA_NAMESPACE + "+notify";
|
||||
|
||||
private static final Map<XMPPConnection, UserAvatarManager> INSTANCES = new WeakHashMap<>();
|
||||
|
||||
private final PepManager pepManager;
|
||||
private final ServiceDiscoveryManager serviceDiscoveryManager;
|
||||
|
||||
private AvatarMetadataStore metadataStore;
|
||||
private final Set<AvatarListener> avatarListeners = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Get the singleton instance of UserAvatarManager.
|
||||
*
|
||||
* @param connection {@link XMPPConnection}.
|
||||
* @return the instance of UserAvatarManager
|
||||
*/
|
||||
public static synchronized UserAvatarManager getInstanceFor(XMPPConnection connection) {
|
||||
UserAvatarManager userAvatarManager = INSTANCES.get(connection);
|
||||
|
||||
if (userAvatarManager == null) {
|
||||
userAvatarManager = new UserAvatarManager(connection);
|
||||
INSTANCES.put(connection, userAvatarManager);
|
||||
}
|
||||
|
||||
return userAvatarManager;
|
||||
}
|
||||
|
||||
private UserAvatarManager(XMPPConnection connection) {
|
||||
super(connection);
|
||||
this.pepManager = PepManager.getInstanceFor(connection);
|
||||
this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if User Avatar publishing is supported by the server.
|
||||
* In order to support User Avatars the server must have support for XEP-0163: Personal Eventing Protocol (PEP).
|
||||
*
|
||||
* @return true if User Avatar is supported by the server.
|
||||
*
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0163.html">XEP-0163: Personal Eventing Protocol</a>
|
||||
*
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public boolean isSupportedByServer()
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
|
||||
return pepManager.isSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce support for User Avatars and start receiving avatar updates.
|
||||
*/
|
||||
public void enable() {
|
||||
pepManager.addPepListener(metadataExtensionListener);
|
||||
serviceDiscoveryManager.addFeature(FEATURE_METADATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop receiving avatar updates.
|
||||
*/
|
||||
public void disable() {
|
||||
serviceDiscoveryManager.removeFeature(FEATURE_METADATA);
|
||||
pepManager.addPepListener(metadataExtensionListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an {@link AvatarMetadataStore} which is used to store information about the local availability of avatar
|
||||
* data.
|
||||
* @param metadataStore metadata store
|
||||
*/
|
||||
public void setAvatarMetadataStore(AvatarMetadataStore metadataStore) {
|
||||
this.metadataStore = metadataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an {@link AvatarListener} in order to be notified about incoming avatar metadata updates.
|
||||
*
|
||||
* @param listener listener
|
||||
* @return true if the set of listeners did not already contain the listener
|
||||
*/
|
||||
public synchronized boolean addAvatarListener(AvatarListener listener) {
|
||||
return avatarListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an {@link AvatarListener} to stop being notified about incoming avatar metadata updates.
|
||||
*
|
||||
* @param listener listener
|
||||
* @return true if the set of listeners contained the listener
|
||||
*/
|
||||
public synchronized boolean removeAvatarListener(AvatarListener listener) {
|
||||
return avatarListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data node.
|
||||
* This node contains the avatar image data.
|
||||
*
|
||||
* @return the data node
|
||||
* @throws NoResponseException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
* @throws XMPPErrorException
|
||||
*/
|
||||
private LeafNode getOrCreateDataNode()
|
||||
throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
|
||||
return pepManager.getPepPubSubManager().getOrCreateLeafNode(DATA_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata node.
|
||||
* This node contains lightweight metadata information about the data in the data node.
|
||||
*
|
||||
* @return the metadata node
|
||||
* @throws NoResponseException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
* @throws XMPPErrorException
|
||||
*/
|
||||
private LeafNode getOrCreateMetadataNode()
|
||||
throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
|
||||
return pepManager.getPepPubSubManager().getOrCreateLeafNode(METADATA_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a PNG Avatar and its metadata to PubSub.
|
||||
*
|
||||
* @param data
|
||||
* @param height
|
||||
* @param width
|
||||
* @throws XMPPErrorException
|
||||
* @throws PubSubException.NotALeafNodeException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
* @throws NoResponseException
|
||||
*/
|
||||
public void publishAvatar(byte[] data, int height, int width)
|
||||
throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
|
||||
InterruptedException, NoResponseException {
|
||||
String id = publishAvatarData(data);
|
||||
publishAvatarMetadata(id, data.length, "image/png", height, width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a PNG avatar and its metadata to PubSub.
|
||||
*
|
||||
* @param pngFile PNG File
|
||||
* @param height height of the image
|
||||
* @param width width of the image
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws XMPPErrorException
|
||||
* @throws PubSubException.NotALeafNodeException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
* @throws NoResponseException
|
||||
*/
|
||||
public void publishAvatar(File pngFile, int height, int width)
|
||||
throws IOException, XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
|
||||
InterruptedException, NoResponseException {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream((int) pngFile.length());
|
||||
InputStream in = new BufferedInputStream(new FileInputStream(pngFile))) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
byte[] bytes = out.toByteArray();
|
||||
publishAvatar(bytes, height, width);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo)
|
||||
throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException,
|
||||
NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException {
|
||||
LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from)
|
||||
.getLeafNode(DATA_NAMESPACE);
|
||||
|
||||
List<PayloadItem<DataExtension>> dataItems = dataNode.getItems(1, metadataInfo.getId());
|
||||
DataExtension extension = dataItems.get(0).getPayload();
|
||||
if (metadataStore != null) {
|
||||
metadataStore.setAvatarAvailable(from, metadataInfo.getId());
|
||||
}
|
||||
return extension.getData();
|
||||
}
|
||||
|
||||
private String publishAvatarData(byte[] data)
|
||||
throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
String itemId = Base64.encodeToString(SHA1.bytes(data));
|
||||
publishAvatarData(data, itemId);
|
||||
return itemId;
|
||||
}
|
||||
|
||||
private void publishAvatarData(byte[] data, String itemId)
|
||||
throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
DataExtension dataExtension = new DataExtension(data);
|
||||
getOrCreateDataNode().publish(new PayloadItem<>(itemId, dataExtension));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish metadata about an avatar to the metadata node.
|
||||
*
|
||||
* @param itemId SHA-1 sum of the image of type image/png
|
||||
* @param info info element containing metadata of the file
|
||||
* @param pointers list of metadata pointer elements
|
||||
*
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void publishAvatarMetadata(String itemId, MetadataInfo info, List<MetadataPointer> pointers)
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
publishAvatarMetadata(itemId, Collections.singletonList(info), pointers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish avatar metadata.
|
||||
*
|
||||
* @param itemId SHA-1 sum of the avatar image representation of type image/png
|
||||
* @param infos list of metadata elements
|
||||
* @param pointers list of pointer elements
|
||||
*
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void publishAvatarMetadata(String itemId, List<MetadataInfo> infos, List<MetadataPointer> pointers)
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
|
||||
getOrCreateMetadataNode().publish(new PayloadItem<>(itemId, metadataExtension));
|
||||
|
||||
if (metadataStore == null) {
|
||||
return;
|
||||
}
|
||||
// Mark our own avatar as locally available so that we don't get updates for it
|
||||
metadataStore.setAvatarAvailable(connection().getUser().asEntityBareJidOrThrow(), itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish metadata about an avatar available via HTTP.
|
||||
* This method can be used together with HTTP File Upload as an alternative to PubSub for avatar publishing.
|
||||
*
|
||||
* @param itemId SHA-1 sum of the avatar image file.
|
||||
* @param url HTTP(S) Url of the image file.
|
||||
* @param bytes size of the file in bytes
|
||||
* @param type content type of the file
|
||||
* @param pixelsHeight height of the image file in pixels
|
||||
* @param pixelsWidth width of the image file in pixels
|
||||
*
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void publishHttpAvatarMetadata(String itemId, URL url, long bytes, String type,
|
||||
int pixelsHeight, int pixelsWidth)
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
MetadataInfo info = new MetadataInfo(itemId, url, bytes, type, pixelsHeight, pixelsWidth);
|
||||
publishAvatarMetadata(itemId, info, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish avatar metadata with its size in pixels.
|
||||
*
|
||||
* @param itemId
|
||||
* @param bytes
|
||||
* @param type
|
||||
* @param pixelsHeight
|
||||
* @param pixelsWidth
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void publishAvatarMetadata(String itemId, long bytes, String type, int pixelsHeight,
|
||||
int pixelsWidth)
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
MetadataInfo info = new MetadataInfo(itemId, null, bytes, type, pixelsHeight, pixelsWidth);
|
||||
publishAvatarMetadata(itemId, info, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an empty metadata element to disable avatar publishing.
|
||||
*
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0084.html#proto-meta">§4.2 Metadata Element</a>
|
||||
*
|
||||
* @throws NoResponseException
|
||||
* @throws XMPPErrorException
|
||||
* @throws NotConnectedException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void unpublishAvatar()
|
||||
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
|
||||
getOrCreateMetadataNode().publish(new PayloadItem<>(new MetadataExtension(null)));
|
||||
}
|
||||
|
||||
private final PepListener metadataExtensionListener = new PepListener() {
|
||||
@Override
|
||||
public void eventReceived(EntityBareJid from, EventElement event, Message message) {
|
||||
if (!MetadataExtension.NAMESPACE.equals(event.getNamespace())) {
|
||||
// Totally not of interest for us.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MetadataExtension.ELEMENT.equals(event.getElementName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ExtensionElement items : event.getExtensions()) {
|
||||
if (!(items instanceof ItemsExtension)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (ExtensionElement item : ((ItemsExtension) items).getExtensions()) {
|
||||
if (!(item instanceof PayloadItem<?>)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PayloadItem<?> payloadItem = (PayloadItem<?>) item;
|
||||
|
||||
if (!(payloadItem.getPayload() instanceof MetadataExtension)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MetadataExtension metadataExtension = (MetadataExtension) payloadItem.getPayload();
|
||||
if (metadataStore != null && metadataStore.hasAvatarAvailable(from, ((PayloadItem<?>) item).getId())) {
|
||||
// The metadata store implies that we have a local copy of the published image already. Skip.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (AvatarListener listener : avatarListeners) {
|
||||
listener.onAvatarUpdateReceived(from, metadataExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez, 2019 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.avatar.element;
|
||||
|
||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||
import org.jivesoftware.smack.packet.XmlEnvironment;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
import org.jivesoftware.smack.util.XmlStringBuilder;
|
||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
||||
import org.jivesoftware.smackx.avatar.UserAvatarManager;
|
||||
|
||||
/**
|
||||
* Data extension element class used to publish avatar image data via PubSub.
|
||||
* Unlike the {@link MetadataExtension}, this class is dedicated to containing the avatar image data itself.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public class DataExtension implements ExtensionElement {
|
||||
|
||||
public static final String ELEMENT = "data";
|
||||
public static final String NAMESPACE = UserAvatarManager.DATA_NAMESPACE;
|
||||
|
||||
private final byte[] data;
|
||||
private String data_b64; // lazy initialized base64 encoded copy of data
|
||||
|
||||
/**
|
||||
* Create a {@link DataExtension} from a byte array.
|
||||
*
|
||||
* @param data bytes of the image.
|
||||
*/
|
||||
public DataExtension(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link DataExtension} from a base64 encoded String.
|
||||
*
|
||||
* @param base64data bytes of the image as base64 string.
|
||||
*/
|
||||
public DataExtension(String base64data) {
|
||||
this.data_b64 = StringUtils.requireNotNullNorEmpty(base64data,
|
||||
"Base64 String MUST NOT be null, nor empty.");
|
||||
this.data = Base64.decode(base64data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bytes of the image.
|
||||
*
|
||||
* @return an immutable copy of the image data
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image data encoded as a base64 String.
|
||||
*
|
||||
* @return the data as String
|
||||
*/
|
||||
public String getDataAsString() {
|
||||
if (data_b64 == null) {
|
||||
data_b64 = Base64.encodeToString(data);
|
||||
}
|
||||
return data_b64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getElementName() {
|
||||
return ELEMENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence toXML(XmlEnvironment xmlEnvironment) {
|
||||
XmlStringBuilder xml = new XmlStringBuilder(this);
|
||||
xml.rightAngleBracket();
|
||||
xml.escape(this.getDataAsString());
|
||||
xml.closeElement(this);
|
||||
return xml;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.avatar.element;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||
import org.jivesoftware.smack.packet.Stanza;
|
||||
import org.jivesoftware.smack.packet.XmlEnvironment;
|
||||
import org.jivesoftware.smack.util.XmlStringBuilder;
|
||||
import org.jivesoftware.smackx.avatar.MetadataInfo;
|
||||
import org.jivesoftware.smackx.avatar.MetadataPointer;
|
||||
import org.jivesoftware.smackx.avatar.UserAvatarManager;
|
||||
|
||||
/**
|
||||
* Metadata extension element class.
|
||||
* This class contains metadata about published avatars.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public class MetadataExtension implements ExtensionElement {
|
||||
|
||||
public static final String ELEMENT = "metadata";
|
||||
public static final String NAMESPACE = UserAvatarManager.METADATA_NAMESPACE;
|
||||
|
||||
private final List<MetadataInfo> infos;
|
||||
private final List<MetadataPointer> pointers;
|
||||
|
||||
/**
|
||||
* Metadata Extension constructor.
|
||||
*
|
||||
* @param infos
|
||||
*/
|
||||
public MetadataExtension(List<MetadataInfo> infos) {
|
||||
this(infos, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata Extension constructor.
|
||||
*
|
||||
* @param infos
|
||||
* @param pointers
|
||||
*/
|
||||
public MetadataExtension(List<MetadataInfo> infos, List<MetadataPointer> pointers) {
|
||||
this.infos = infos;
|
||||
this.pointers = pointers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the info elements list.
|
||||
*
|
||||
* @return the info elements list
|
||||
*/
|
||||
public List<MetadataInfo> getInfoElements() {
|
||||
return Collections.unmodifiableList(infos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pointer elements list.
|
||||
*
|
||||
* @return the pointer elements list
|
||||
*/
|
||||
public List<MetadataPointer> getPointerElements() {
|
||||
return (pointers == null) ? null : Collections.unmodifiableList(pointers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getElementName() {
|
||||
return ELEMENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence toXML(XmlEnvironment xmlEnvironment) {
|
||||
XmlStringBuilder xml = new XmlStringBuilder(this);
|
||||
appendInfoElements(xml);
|
||||
appendPointerElements(xml);
|
||||
closeElement(xml);
|
||||
return xml;
|
||||
}
|
||||
|
||||
private void appendInfoElements(XmlStringBuilder xml) {
|
||||
if (infos != null) {
|
||||
xml.rightAngleBracket();
|
||||
|
||||
for (MetadataInfo info : infos) {
|
||||
xml.halfOpenElement("info");
|
||||
xml.attribute("id", info.getId());
|
||||
xml.attribute("bytes", info.getBytes().longValue());
|
||||
xml.attribute("type", info.getType());
|
||||
xml.optAttribute("url", info.getUrl());
|
||||
|
||||
if (info.getHeight().nativeRepresentation() > 0) {
|
||||
xml.attribute("height", info.getHeight().nativeRepresentation());
|
||||
}
|
||||
|
||||
if (info.getWidth().nativeRepresentation() > 0) {
|
||||
xml.attribute("width", info.getWidth().nativeRepresentation());
|
||||
}
|
||||
|
||||
xml.closeEmptyElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendPointerElements(XmlStringBuilder xml) {
|
||||
if (pointers != null) {
|
||||
|
||||
for (MetadataPointer pointer : pointers) {
|
||||
xml.openElement("pointer");
|
||||
xml.halfOpenElement("x");
|
||||
|
||||
String namespace = pointer.getNamespace();
|
||||
if (namespace != null) {
|
||||
xml.xmlnsAttribute(namespace);
|
||||
}
|
||||
|
||||
xml.rightAngleBracket();
|
||||
|
||||
Map<String, Object> fields = pointer.getFields();
|
||||
if (fields != null) {
|
||||
for (Map.Entry<String, Object> pair : fields.entrySet()) {
|
||||
xml.escapedElement(pair.getKey(), String.valueOf(pair.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
xml.closeElement("x");
|
||||
xml.closeElement("pointer");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void closeElement(XmlStringBuilder xml) {
|
||||
if (infos != null || pointers != null) {
|
||||
xml.closeElement(this);
|
||||
} else {
|
||||
xml.closeEmptyElement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true, if this {@link MetadataExtension} is to be interpreted as Avatar unpublishing.
|
||||
*
|
||||
* @return true if unpublishing, false otherwise
|
||||
*/
|
||||
public boolean isDisablingPublishing() {
|
||||
return getInfoElements().isEmpty();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* User Avatar elements.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
package org.jivesoftware.smackx.avatar.element;
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2019 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.avatar.listener;
|
||||
|
||||
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
|
||||
|
||||
import org.jxmpp.jid.EntityBareJid;
|
||||
|
||||
/**
|
||||
* Listener that can notify the user about User Avatar updates.
|
||||
*
|
||||
* @author Paul Schaub
|
||||
*/
|
||||
public interface AvatarListener {
|
||||
|
||||
void onAvatarUpdateReceived(EntityBareJid user, MetadataExtension metadata);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
/**
|
||||
* Classes and interfaces of User Avatar.
|
||||
*
|
||||
* @author Paul Schaub
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
package org.jivesoftware.smackx.avatar.listener;
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Classes and interfaces of User Avatar.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
package org.jivesoftware.smackx.avatar;
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.avatar.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jivesoftware.smack.packet.XmlEnvironment;
|
||||
import org.jivesoftware.smack.provider.ExtensionElementProvider;
|
||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
||||
import org.jivesoftware.smack.xml.XmlPullParser;
|
||||
import org.jivesoftware.smack.xml.XmlPullParserException;
|
||||
import org.jivesoftware.smackx.avatar.element.DataExtension;
|
||||
|
||||
/**
|
||||
* User Avatar data provider class.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public class DataProvider extends ExtensionElementProvider<DataExtension> {
|
||||
|
||||
@Override
|
||||
public DataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment environment)
|
||||
throws IOException, XmlPullParserException {
|
||||
byte[] data = Base64.decode(parser.nextText());
|
||||
return new DataExtension(data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez, 2019 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.avatar.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.jivesoftware.smack.packet.XmlEnvironment;
|
||||
import org.jivesoftware.smack.provider.ExtensionElementProvider;
|
||||
import org.jivesoftware.smack.xml.XmlPullParser;
|
||||
import org.jivesoftware.smack.xml.XmlPullParserException;
|
||||
import org.jivesoftware.smackx.avatar.MetadataInfo;
|
||||
import org.jivesoftware.smackx.avatar.MetadataPointer;
|
||||
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
|
||||
|
||||
/**
|
||||
* User Avatar metadata provider class.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @author Paul Schaub
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
public class MetadataProvider extends ExtensionElementProvider<MetadataExtension> {
|
||||
|
||||
@Override
|
||||
public MetadataExtension parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws IOException, XmlPullParserException {
|
||||
List<MetadataInfo> metadataInfos = null;
|
||||
List<MetadataPointer> pointers = null;
|
||||
|
||||
while (true) {
|
||||
XmlPullParser.Event eventType = parser.next();
|
||||
|
||||
if (eventType == XmlPullParser.Event.START_ELEMENT) {
|
||||
|
||||
if (parser.getName().equals("info")) {
|
||||
if (metadataInfos == null) {
|
||||
metadataInfos = new ArrayList<>();
|
||||
}
|
||||
|
||||
MetadataInfo info = parseInfo(parser);
|
||||
if (info.getId() != null) {
|
||||
metadataInfos.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.getName().equals("pointer")) {
|
||||
if (pointers == null) {
|
||||
pointers = new ArrayList<>();
|
||||
}
|
||||
|
||||
pointers.add(parsePointer(parser));
|
||||
}
|
||||
|
||||
} else if (eventType == XmlPullParser.Event.END_ELEMENT) {
|
||||
if (parser.getDepth() == initialDepth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new MetadataExtension(metadataInfos, pointers);
|
||||
}
|
||||
|
||||
private MetadataInfo parseInfo(XmlPullParser parser) throws XmlPullParserException {
|
||||
String id;
|
||||
URL url = null;
|
||||
long bytes = 0;
|
||||
String type;
|
||||
int pixelsHeight = 0;
|
||||
int pixelsWidth = 0;
|
||||
|
||||
id = parser.getAttributeValue("", "id");
|
||||
type = parser.getAttributeValue("", "type");
|
||||
String urlString = parser.getAttributeValue("", "url");
|
||||
if (urlString != null && !urlString.isEmpty()) {
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new XmlPullParserException("Cannot parse URL '" + urlString + "'");
|
||||
}
|
||||
}
|
||||
|
||||
String bytesString = parser.getAttributeValue("", "bytes");
|
||||
if (bytesString != null) {
|
||||
bytes = Long.parseLong(bytesString);
|
||||
}
|
||||
|
||||
String widthString = parser.getAttributeValue("", "width");
|
||||
if (widthString != null) {
|
||||
pixelsWidth = Integer.parseInt(widthString);
|
||||
}
|
||||
|
||||
String heightString = parser.getAttributeValue("", "height");
|
||||
if (heightString != null) {
|
||||
pixelsHeight = Integer.parseInt(heightString);
|
||||
}
|
||||
|
||||
try {
|
||||
return new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new XmlPullParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private MetadataPointer parsePointer(XmlPullParser parser) throws XmlPullParserException, IOException {
|
||||
int pointerDepth = parser.getDepth();
|
||||
String namespace = null;
|
||||
HashMap<String, Object> fields = null;
|
||||
|
||||
while (true) {
|
||||
XmlPullParser.Event eventType2 = parser.next();
|
||||
|
||||
if (eventType2 == XmlPullParser.Event.START_ELEMENT) {
|
||||
if (parser.getName().equals("x")) {
|
||||
namespace = parser.getNamespace();
|
||||
} else {
|
||||
if (fields == null) {
|
||||
fields = new HashMap<>();
|
||||
}
|
||||
|
||||
String name = parser.getName();
|
||||
Object value = parser.nextText();
|
||||
fields.put(name, value);
|
||||
}
|
||||
} else if (eventType2 == XmlPullParser.Event.END_ELEMENT) {
|
||||
if (parser.getDepth() == pointerDepth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new MetadataPointer(namespace, fields);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* User Avatar providers.
|
||||
*
|
||||
* @author Fernando Ramirez
|
||||
* @see <a href="http://xmpp.org/extensions/xep-0084.html">XEP-0084: User
|
||||
* Avatar</a>
|
||||
*/
|
||||
package org.jivesoftware.smackx.avatar.provider;
|
|
@ -596,4 +596,15 @@
|
|||
<className>org.jivesoftware.smackx.usertune.provider.UserTuneProvider</className>
|
||||
</extensionProvider>
|
||||
|
||||
<!-- XEP-0084: User Avatar -->
|
||||
<extensionProvider>
|
||||
<elementName>data</elementName>
|
||||
<namespace>urn:xmpp:avatar:data</namespace>
|
||||
<className>org.jivesoftware.smackx.avatar.provider.DataProvider</className>
|
||||
</extensionProvider>
|
||||
<extensionProvider>
|
||||
<elementName>metadata</elementName>
|
||||
<namespace>urn:xmpp:avatar:metadata</namespace>
|
||||
<className>org.jivesoftware.smackx.avatar.provider.MetadataProvider</className>
|
||||
</extensionProvider>
|
||||
</smackProviders>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.avatar;
|
||||
|
||||
import org.jivesoftware.smack.test.util.SmackTestSuite;
|
||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
||||
import org.jivesoftware.smack.xml.XmlPullParser;
|
||||
import org.jivesoftware.smackx.avatar.element.DataExtension;
|
||||
import org.jivesoftware.smackx.avatar.provider.DataProvider;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DataExtensionTest extends SmackTestSuite {
|
||||
|
||||
// @formatter:off
|
||||
String dataExtensionExample = "<data xmlns='urn:xmpp:avatar:data'>"
|
||||
+ "qANQR1DBwU4DX7jmYZnnfe32"
|
||||
+ "</data>";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
public void checkDataExtensionParse() throws Exception {
|
||||
byte[] data = Base64.decode("qANQR1DBwU4DX7jmYZnnfe32");
|
||||
DataExtension dataExtension = new DataExtension(data);
|
||||
Assert.assertEquals(dataExtensionExample, dataExtension.toXML().toString());
|
||||
|
||||
XmlPullParser parser = PacketParserUtils.getParserFor(dataExtensionExample);
|
||||
DataExtension dataExtensionFromProvider = new DataProvider().parse(parser);
|
||||
Assert.assertEquals(Base64.encodeToString(data), Base64.encodeToString(dataExtensionFromProvider.getData()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2017 Fernando Ramirez
|
||||
*
|
||||
* 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.avatar;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||
import org.jivesoftware.smack.xml.XmlPullParser;
|
||||
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
|
||||
import org.jivesoftware.smackx.avatar.provider.MetadataProvider;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MetadataExtensionTest {
|
||||
|
||||
private static final String metadataExtensionExample = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
|
||||
+ "<info "
|
||||
+ "id='357a8123a30844a3aa99861b6349264ba67a5694' "
|
||||
+ "bytes='23456' "
|
||||
+ "type='image/gif' "
|
||||
+ "url='http://avatars.example.org/happy.gif' "
|
||||
+ "height='64' "
|
||||
+ "width='128'/>"
|
||||
+ "</metadata>";
|
||||
|
||||
private static final String emptyMetadataExtensionExample = "<metadata xmlns='urn:xmpp:avatar:metadata'/>";
|
||||
|
||||
private static final String metadataWithSeveralInfos = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
|
||||
+ "<info bytes='12345'"
|
||||
+ " height='64'"
|
||||
+ " id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'"
|
||||
+ " type='image/png'"
|
||||
+ " width='64'/>"
|
||||
+ "<info bytes='12345'"
|
||||
+ " height='64'"
|
||||
+ " id='e279f80c38f99c1e7e53e262b440993b2f7eea57'"
|
||||
+ " type='image/png'"
|
||||
+ " url='http://avatars.example.org/happy.png'"
|
||||
+ " width='128'/>"
|
||||
+ "<info bytes='23456'"
|
||||
+ " height='64'"
|
||||
+ " id='357a8123a30844a3aa99861b6349264ba67a5694'"
|
||||
+ " type='image/gif'"
|
||||
+ " url='http://avatars.example.org/happy.gif'"
|
||||
+ " width='64'/>"
|
||||
+ "</metadata>";
|
||||
|
||||
private static final String metadataWithInfoAndPointers = "<metadata xmlns='urn:xmpp:avatar:metadata'>"
|
||||
+ "<info"
|
||||
+ " id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'"
|
||||
+ " bytes='12345'"
|
||||
+ " type='image/png'"
|
||||
+ " height='64'"
|
||||
+ " width='64'/>"
|
||||
+ "<pointer>"
|
||||
+ "<x xmlns='http://example.com/virtualworlds'>"
|
||||
+ "<game>Ancapistan</game>"
|
||||
+ "<character>Kropotkin</character>"
|
||||
+ "</x>"
|
||||
+ "</pointer>"
|
||||
+ "<pointer>"
|
||||
+ "<x xmlns='http://sample.com/game'>"
|
||||
+ "<level>hard</level>"
|
||||
+ "<players>2</players>"
|
||||
+ "</x>"
|
||||
+ "</pointer>"
|
||||
+ "</metadata>";
|
||||
|
||||
@Test
|
||||
public void checkMetadataExtensionParse() throws Exception {
|
||||
String id = "357a8123a30844a3aa99861b6349264ba67a5694";
|
||||
URL url = new URL("http://avatars.example.org/happy.gif");
|
||||
long bytes = 23456;
|
||||
String type = "image/gif";
|
||||
int pixelsHeight = 64;
|
||||
int pixelsWidth = 128;
|
||||
|
||||
MetadataInfo info = new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
|
||||
List<MetadataInfo> infos = new ArrayList<>();
|
||||
infos.add(info);
|
||||
|
||||
MetadataExtension metadataExtension = new MetadataExtension(infos);
|
||||
Assert.assertEquals(metadataExtensionExample, metadataExtension.toXML().toString());
|
||||
|
||||
XmlPullParser parser = PacketParserUtils.getParserFor(metadataExtensionExample);
|
||||
MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
|
||||
|
||||
Assert.assertEquals(id, metadataExtensionFromProvider.getInfoElements().get(0).getId());
|
||||
Assert.assertEquals(url, metadataExtensionFromProvider.getInfoElements().get(0).getUrl());
|
||||
Assert.assertEquals(bytes, metadataExtensionFromProvider.getInfoElements().get(0).getBytes().intValue());
|
||||
Assert.assertEquals(type, metadataExtensionFromProvider.getInfoElements().get(0).getType());
|
||||
Assert.assertEquals(pixelsHeight, metadataExtensionFromProvider.getInfoElements().get(0).getHeight().intValue());
|
||||
Assert.assertEquals(pixelsWidth, metadataExtensionFromProvider.getInfoElements().get(0).getWidth().intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkEmptyMetadataExtensionParse() throws Exception {
|
||||
MetadataExtension metadataExtension = new MetadataExtension(null);
|
||||
Assert.assertEquals(emptyMetadataExtensionExample, metadataExtension.toXML().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkSeveralInfosInMetadataExtension() throws Exception {
|
||||
XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithSeveralInfos);
|
||||
MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
|
||||
|
||||
MetadataInfo info1 = metadataExtensionFromProvider.getInfoElements().get(0);
|
||||
MetadataInfo info2 = metadataExtensionFromProvider.getInfoElements().get(1);
|
||||
MetadataInfo info3 = metadataExtensionFromProvider.getInfoElements().get(2);
|
||||
|
||||
Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info1.getId());
|
||||
Assert.assertNull(info1.getUrl());
|
||||
Assert.assertEquals(12345, info1.getBytes().intValue());
|
||||
Assert.assertEquals("image/png", info1.getType());
|
||||
Assert.assertEquals(64, info1.getHeight().intValue());
|
||||
Assert.assertEquals(64, info1.getWidth().intValue());
|
||||
|
||||
Assert.assertEquals("e279f80c38f99c1e7e53e262b440993b2f7eea57", info2.getId());
|
||||
Assert.assertEquals(new URL("http://avatars.example.org/happy.png"), info2.getUrl());
|
||||
Assert.assertEquals(12345, info2.getBytes().intValue());
|
||||
Assert.assertEquals("image/png", info2.getType());
|
||||
Assert.assertEquals(64, info2.getHeight().intValue());
|
||||
Assert.assertEquals(128, info2.getWidth().intValue());
|
||||
|
||||
Assert.assertEquals("357a8123a30844a3aa99861b6349264ba67a5694", info3.getId());
|
||||
Assert.assertEquals(new URL("http://avatars.example.org/happy.gif"), info3.getUrl());
|
||||
Assert.assertEquals(23456, info3.getBytes().intValue());
|
||||
Assert.assertEquals("image/gif", info3.getType());
|
||||
Assert.assertEquals(64, info3.getHeight().intValue());
|
||||
Assert.assertEquals(64, info3.getWidth().intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkInfosAndPointersParse() throws Exception {
|
||||
XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithInfoAndPointers);
|
||||
MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
|
||||
|
||||
MetadataInfo info = metadataExtensionFromProvider.getInfoElements().get(0);
|
||||
Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info.getId());
|
||||
Assert.assertNull(info.getUrl());
|
||||
Assert.assertEquals(12345, info.getBytes().intValue());
|
||||
Assert.assertEquals("image/png", info.getType());
|
||||
Assert.assertEquals(64, info.getHeight().intValue());
|
||||
Assert.assertEquals(64, info.getWidth().intValue());
|
||||
|
||||
MetadataPointer pointer1 = metadataExtensionFromProvider.getPointerElements().get(0);
|
||||
Map<String, Object> fields1 = pointer1.getFields();
|
||||
Assert.assertEquals("http://example.com/virtualworlds", pointer1.getNamespace());
|
||||
Assert.assertEquals("Ancapistan", fields1.get("game"));
|
||||
Assert.assertEquals("Kropotkin", fields1.get("character"));
|
||||
|
||||
MetadataPointer pointer2 = metadataExtensionFromProvider.getPointerElements().get(1);
|
||||
Map<String, Object> fields2 = pointer2.getFields();
|
||||
Assert.assertEquals("http://sample.com/game", pointer2.getNamespace());
|
||||
Assert.assertEquals("hard", fields2.get("level"));
|
||||
Assert.assertEquals("2", fields2.get("players"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMetadataExtensionWithInfoAndPointer() {
|
||||
String id = "111f4b3c50d7b0df729d299bc6f8e9ef9066971f";
|
||||
long bytes = 12345;
|
||||
String type = "image/png";
|
||||
int pixelsHeight = 64;
|
||||
int pixelsWidth = 64;
|
||||
MetadataInfo info = new MetadataInfo(id, null, bytes, type, pixelsHeight, pixelsWidth);
|
||||
|
||||
HashMap<String, Object> fields1 = new HashMap<>();
|
||||
fields1.put("game", "Ancapistan");
|
||||
fields1.put("character", "Kropotkin");
|
||||
MetadataPointer pointer1 = new MetadataPointer("http://example.com/virtualworlds", fields1);
|
||||
|
||||
HashMap<String, Object> fields2 = new HashMap<>();
|
||||
fields2.put("level", "hard");
|
||||
fields2.put("players", 2);
|
||||
MetadataPointer pointer2 = new MetadataPointer("http://sample.com/game", fields2);
|
||||
|
||||
List<MetadataInfo> infos = new ArrayList<>();
|
||||
infos.add(info);
|
||||
|
||||
List<MetadataPointer> pointers = new ArrayList<>();
|
||||
pointers.add(pointer1);
|
||||
pointers.add(pointer2);
|
||||
|
||||
MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
|
||||
Assert.assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue