XEP-0084: User Avatar

Co-authored-by: Paul Schaub <vanitasvitae@fsfe.org>
This commit is contained in:
Fernando Ramirez 2021-01-28 18:25:25 +01:00 committed by Paul Schaub
parent 1df0763f92
commit 0f5297c957
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
23 changed files with 2045 additions and 0 deletions

View File

@ -58,6 +58,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. |

View File

@ -18,6 +18,7 @@ package org.jivesoftware.smack.util;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
@ -361,6 +362,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;
}
/**
* Same as {@link #optAttribute(String, CharSequence)}, but with a different method name. This method can be used if
* the provided attribute value argument type causes ambiguity in method overloading. For example if the type is a

View File

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

View File

@ -0,0 +1,84 @@
/**
*
* Copyright 2020 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.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.smack.util.HashCode;
import org.jivesoftware.smack.util.Objects;
import org.jxmpp.jid.EntityBareJid;
public class MemoryAvatarMetadataStore implements AvatarMetadataStore {
private Map<Tuple<EntityBareJid, String>, Boolean> availabilityMap = new ConcurrentHashMap<>();
@Override
public boolean hasAvatarAvailable(EntityBareJid jid, String itemId) {
Boolean available = availabilityMap.get(new Tuple<>(jid, itemId));
return available != null && available;
}
@Override
public void setAvatarAvailable(EntityBareJid jid, String itemId) {
availabilityMap.put(new Tuple<>(jid, itemId), Boolean.TRUE);
}
private static class Tuple<A, B> {
private final A first;
private final B second;
Tuple(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() {
return first;
}
public B getSecond() {
return second;
}
@Override
public int hashCode() {
return HashCode.builder()
.append(first)
.append(second)
.build();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof Tuple)) {
return false;
}
@SuppressWarnings("unchecked") Tuple<A, B> other = (Tuple<A, B>) obj;
return Objects.equals(getFirst(), other.getFirst())
&& Objects.equals(getSecond(), other.getSecond());
}
}
}

View File

@ -0,0 +1,127 @@
/**
*
* 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;
if (bytes <= 0) {
throw new IllegalArgumentException("Number of bytes MUST be greater than 0.");
}
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;
}
}

View File

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

View File

@ -0,0 +1,66 @@
/**
*
* 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;
public class UserAvatarException extends AssertionError {
private static final long serialVersionUID = 1L;
private UserAvatarException(String message) {
super(message);
}
/**
* Exception that gets thrown, when the {@link org.jivesoftware.smackx.avatar.element.DataExtension DataExtensions}
* byte count does not match the {@link org.jivesoftware.smackx.avatar.element.MetadataExtension MetadataExtensions}
* 'bytes' value.
*/
public static class AvatarMetadataMismatchException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public AvatarMetadataMismatchException(String message) {
super(message);
}
}
/**
* Exception that gets thrown when the user tries to publish a {@link org.jivesoftware.smackx.avatar.element.MetadataExtension}
* that is missing a required {@link MetadataInfo} for an image of type {@link UserAvatarManager#TYPE_PNG}.
*/
public static class AvatarMetadataMissingPNGInfoException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public AvatarMetadataMissingPNGInfoException(String message) {
super(message);
}
}
/**
* Exception that gets thrown when the user tries to fetch a {@link org.jivesoftware.smackx.avatar.element.DataExtension}
* from PubSub using a {@link MetadataInfo} that does point to a HTTP resource.
*/
public static class NotAPubSubAvatarInfoElementException extends UserAvatarException {
private static final long serialVersionUID = 1L;
public NotAPubSubAvatarInfoElementException(String message) {
super(message);
}
}
}

View File

@ -0,0 +1,504 @@
/**
*
* 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.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.PepEventListener;
import org.jivesoftware.smackx.pep.PepManager;
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;
/**
* <h1>User Avatar manager class.</h1>
* This manager allows publication of user avatar images via PubSub, as well as publication of
* {@link MetadataExtension MetadataExtensions} containing {@link MetadataInfo} elements for avatars
* available via PubSub, HTTP or external third-party services via {@link MetadataPointer} elements.
* <p>
* The easiest way to publish a PNG avatar (support for PNG files is REQUIRED), is to use
* {@link #publishPNGAvatar(File, int, int)} which will publish the image data to PubSub and inform
* any subscribers via a metadata update.
* <p>
* Uploading avatars via HTTP is not in the scope of this manager (you could use Smacks
* HTTPFileUploadManager from smack-experimental for that), but publishing metadata updates pointing
* to HTTP resources is supported. Use {@link #publishHttpPNGAvatarMetadata(String, URL, long, int, int)} for that.
* <p>
* By calling {@link #enable()}, the {@link UserAvatarManager} will start receiving metadata updates from
* contacts and other entities. If you want to get informed about those updates, you can register listeners
* by calling {@link #addAvatarListener(AvatarListener)}.
* <p>
* If you store avatars locally, it is recommended to also set an {@link AvatarMetadataStore}, which is responsible
* for keeping track of which avatar files are available locally. If you register such a store via
* {@link #setAvatarMetadataStore(AvatarMetadataStore)}, your registered {@link AvatarListener AvatarListeners}
* will only inform you about those avatars that are not yet locally available.
* <p>
* To fetch an avatar from PubSub, use {@link #fetchAvatarFromPubSub(EntityBareJid, MetadataInfo)} which will
* retrieve the avatar data from PubSub and mark the avatar as locally available in the {@link AvatarMetadataStore}
* if one is registered.
* <p>
* Fetching avatars published via HTTP is out of scope of this manager. If you do implement it, remember to mark the
* avatar as locally available in your {@link AvatarMetadataStore} after you retrieved it.
*
* @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<>();
public static final String TYPE_PNG = "image/png";
public static final String TYPE_GIF = "image/gif";
public static final String TYPE_JPEG = "image/jpeg";
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 if the server does not respond
* @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
*/
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.addPepEventListener(FEATURE_METADATA, MetadataExtension.class, metadataExtensionListener);
serviceDiscoveryManager.addFeature(FEATURE_METADATA);
}
/**
* Stop receiving avatar updates.
*/
public void disable() {
serviceDiscoveryManager.removeFeature(FEATURE_METADATA);
pepManager.removePepEventListener(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);
}
private LeafNode getOrCreateDataNode()
throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException, PubSubException.NotALeafNodeException {
return pepManager.getPepPubSubManager().getOrCreateLeafNode(DATA_NAMESPACE);
}
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.
* If you know what the dimensions of the image are, use {@link #publishPNGAvatar(byte[], int, int)} instead.
*
* @param data raw bytes of the avatar
*
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a
* {@link LeafNode}
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/
public void publishPNGAvatar(byte[] data) throws XMPPErrorException, PubSubException.NotALeafNodeException,
NotConnectedException, InterruptedException, NoResponseException {
publishPNGAvatar(data, 0, 0);
}
/**
* Publish a PNG Avatar and its metadata to PubSub.
*
* @param data raw bytes of the avatar
* @param height height of the image in pixels
* @param width width of the image in pixels
*
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a
* {@link LeafNode}
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/
public void publishPNGAvatar(byte[] data, int height, int width)
throws XMPPErrorException, PubSubException.NotALeafNodeException, NotConnectedException,
InterruptedException, NoResponseException {
String id = publishPNGAvatarData(data);
publishPNGAvatarMetadata(id, data.length, height, width);
}
/**
* Publish a PNG avatar and its metadata to PubSub.
* If you know the dimensions of the image, use {@link #publishPNGAvatar(File, int, int)} instead.
*
* @param pngFile PNG File
*
* @throws IOException if an {@link IOException} occurs while reading the file
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a valid
* {@link LeafNode}
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/
public void publishPNGAvatar(File pngFile) throws NotConnectedException, InterruptedException,
PubSubException.NotALeafNodeException, NoResponseException, IOException, XMPPErrorException {
publishPNGAvatar(pngFile, 0, 0);
}
/**
* 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 if an {@link IOException} occurs while reading the file
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotALeafNodeException if either the metadata node or the data node is not a valid
* {@link LeafNode}
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws NoResponseException if the server does not respond
*/
public void publishPNGAvatar(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();
publishPNGAvatar(bytes, height, width);
}
}
/**
* Fetch a published user avatar from their PubSub service.
*
* @param from {@link EntityBareJid} of the avatars owner
* @param metadataInfo {@link MetadataInfo} of the avatar that shall be fetched
*
* @return bytes of the avatar
*
* @throws InterruptedException if the thread gets interrupted
* @throws PubSubException.NotALeafNodeException if the data node is not a {@link LeafNode}
* @throws NoResponseException if the server does not respond
* @throws NotConnectedException if the connection is not connected
* @throws XMPPErrorException if a protocol level error occurs
* @throws PubSubException.NotAPubSubNodeException if the data node is not a valid PubSub node
* @throws UserAvatarException.AvatarMetadataMismatchException if the data in the data node does not match whats
* promised by the {@link MetadataInfo} element
* @throws UserAvatarException.NotAPubSubAvatarInfoElementException if the user tries to fetch the avatar using an
* info element that points to a HTTP resource
*/
public byte[] fetchAvatarFromPubSub(EntityBareJid from, MetadataInfo metadataInfo)
throws InterruptedException, PubSubException.NotALeafNodeException, NoResponseException,
NotConnectedException, XMPPErrorException, PubSubException.NotAPubSubNodeException,
UserAvatarException.AvatarMetadataMismatchException {
if (metadataInfo.getUrl() != null) {
throw new UserAvatarException.NotAPubSubAvatarInfoElementException("Provided MetadataInfo element points to " +
"a HTTP resource, not to a PubSub item.");
}
LeafNode dataNode = PubSubManager.getInstanceFor(connection(), from)
.getLeafNode(DATA_NAMESPACE);
List<PayloadItem<DataExtension>> dataItems = dataNode.getItems(1, metadataInfo.getId());
DataExtension data = dataItems.get(0).getPayload();
if (data.getData().length != metadataInfo.getBytes().intValue()) {
throw new UserAvatarException.AvatarMetadataMismatchException("Avatar Data with itemId '" + metadataInfo.getId() +
"' of " + from.asUnescapedString() + " does not match the Metadata (metadata promises " +
metadataInfo.getBytes().intValue() + " bytes, data contains " + data.getData().length + " bytes)");
}
if (metadataStore != null) {
metadataStore.setAvatarAvailable(from, metadataInfo.getId());
}
return data.getData();
}
private String publishPNGAvatarData(byte[] data)
throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException,
PubSubException.NotALeafNodeException {
String itemId = Base64.encodeToString(SHA1.bytes(data));
publishAvatarData(data, itemId);
return itemId;
}
/**
* Publish some avatar image data to PubSub.
* Note, that if the image is an image of type {@link #TYPE_PNG}, the itemId MUST be the SHA-1 sum of that image.
* If however the image is not of type {@link #TYPE_PNG}, the itemId MUST be the SHA-1 sum of the PNG encoded
* representation of this image (an avatar can be published in several image formats, but at least one of them
* must be of type {@link #TYPE_PNG}).
*
* @param data raw bytes of the image
* @param itemId SHA-1 sum of the PNG encoded representation of this image.
*
* @throws NoResponseException if the server does not respond
* @throws NotConnectedException if the connection is not connected
* @throws XMPPErrorException if a protocol level error occurs
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the data node is not a {@link LeafNode}.
*/
public 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 of type {@link #TYPE_PNG} to the metadata node.
*
* @param itemId SHA-1 sum of the image of type {@link #TYPE_PNG}
* @param info info element containing metadata of the file
* @param pointers optional list of metadata pointer elements
*
* @throws NoResponseException if the server does not respond
* @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException of the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws UserAvatarException.AvatarMetadataMissingPNGInfoException if the info element does not point to an
* avatar image of type {@link #TYPE_PNG} available in PubSub.
*/
public void publishPNGAvatarMetadata(String itemId, MetadataInfo info, List<MetadataPointer> pointers)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
publishAvatarMetadata(itemId, Collections.singletonList(info), pointers);
}
/**
* Publish avatar metadata.
* The list of {@link MetadataInfo} elements can contain info about several image data types. However, there must
* be at least one {@link MetadataInfo} element about an image of type {@link #TYPE_PNG} which is destined to
* publication in PubSub. Its id MUST equal the itemId parameter.
*
* @param itemId SHA-1 sum of the avatar image representation of type {@link #TYPE_PNG}
* @param infos list of metadata elements
* @param pointers optional list of pointer elements
*
* @throws NoResponseException if the server does not respond
* @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws UserAvatarException.AvatarMetadataMissingPNGInfoException if the list of {@link MetadataInfo} elements
* does not contain at least one PNG image
*
* @see <a href="https://xmpp.org/extensions/xep-0084.html#proto-info">
* §4.2.1 Info Element - About the restriction that at least one info element must describe a PNG image.</a>
*/
public void publishAvatarMetadata(String itemId, List<MetadataInfo> infos, List<MetadataPointer> pointers)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
// Check if metadata extension contains at least one png image available in PubSub
boolean containsPng = false;
for (MetadataInfo info : infos) {
if (TYPE_PNG.equals(info.getType())) {
containsPng = true;
break;
}
}
if (!containsPng) {
throw new UserAvatarException.AvatarMetadataMissingPNGInfoException(
"The MetadataExtension must contain at least one info element describing an image of type " +
"\"" + TYPE_PNG + "\"");
}
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 a PNG 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 pixelsHeight height of the image file in pixels
* @param pixelsWidth width of the image file in pixels
*
* @throws NoResponseException if the server does not respond
* @throws XMPPErrorException of a protocol level error occurs
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
*/
public void publishHttpPNGAvatarMetadata(String itemId, URL url, long bytes,
int pixelsHeight, int pixelsWidth)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
MetadataInfo info = new MetadataInfo(itemId, url, bytes, TYPE_PNG, pixelsHeight, pixelsWidth);
publishPNGAvatarMetadata(itemId, info, null);
}
/**
* Publish avatar metadata about a PNG avatar with its size in pixels.
*
* @param itemId SHA-1 hash of the PNG encoded image
* @param bytes number of bytes of this particular image data array
* @param pixelsHeight height of this image in pixels
* @param pixelsWidth width of this image in pixels
*
* @throws NoResponseException if the server does not respond
* @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException if the connection is not connected
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
* @throws InterruptedException if the thread is interrupted
*/
public void publishPNGAvatarMetadata(String itemId, long bytes, int pixelsHeight, int pixelsWidth)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
MetadataInfo info = new MetadataInfo(itemId, null, bytes, TYPE_PNG, pixelsHeight, pixelsWidth);
publishPNGAvatarMetadata(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 if the server does not respond
* @throws XMPPErrorException if a protocol level error occurs
* @throws NotConnectedException if the connection is not connected
* @throws InterruptedException if the thread is interrupted
* @throws PubSubException.NotALeafNodeException if the metadata node is not a {@link LeafNode}
*/
public void unpublishAvatar()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, PubSubException.NotALeafNodeException {
getOrCreateMetadataNode().publish(new PayloadItem<>(new MetadataExtension(null)));
}
@SuppressWarnings("UnnecessaryAnonymousClass")
private final PepEventListener<MetadataExtension> metadataExtensionListener = new PepEventListener<MetadataExtension>() {
@Override
public void onPepEvent(EntityBareJid from, MetadataExtension event, String id, Message carrierMessage) {
if (metadataStore != null && metadataStore.hasAvatarAvailable(from, id)) {
// The metadata store implies that we have a local copy of the published image already. Skip.
return;
}
for (AvatarListener listener : avatarListeners) {
listener.onAvatarUpdateReceived(from, event);
}
}
};
}

View File

@ -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 XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
xml.rightAngleBracket();
xml.escape(this.getDataAsString());
xml.closeElement(this);
return xml;
}
}

View File

@ -0,0 +1,175 @@
/**
*
* 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 java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jivesoftware.smack.packet.ExtensionElement;
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
* @author Paul Schaub
* @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 list of {@link MetadataInfo} elements.
*/
public MetadataExtension(List<MetadataInfo> infos) {
this(infos, null);
}
/**
* Metadata Extension constructor.
*
* @param infos list of {@link MetadataInfo} elements
* @param pointers optional list of {@link MetadataPointer} elements
*/
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);
}
/**
* 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();
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
appendInfoElements(xml);
appendPointerElements(xml);
closeElement(xml);
return xml;
}
private void appendInfoElements(XmlStringBuilder xml) {
if (infos == null) {
return;
}
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) {
return;
}
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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,156 @@
/**
*
* 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;
outerloop: while (true) {
XmlPullParser.TagEvent eventType = parser.nextTag();
switch (eventType) {
case 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));
}
break;
case END_ELEMENT:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
}
}
return new MetadataExtension(metadataInfos, pointers);
}
private static 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 static MetadataPointer parsePointer(XmlPullParser parser) throws XmlPullParserException, IOException {
int pointerDepth = parser.getDepth();
String namespace = null;
HashMap<String, Object> fields = null;
outperloop: while (true) {
XmlPullParser.TagEvent tag = parser.nextTag();
switch (tag) {
case 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);
}
break;
case END_ELEMENT:
if (parser.getDepth() == pointerDepth) {
break outperloop;
}
}
}
return new MetadataPointer(namespace, fields);
}
}

View File

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

View File

@ -317,6 +317,18 @@
<className>org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider</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>
<!-- XEP-0085: Chat State Notifications -->
<extensionProvider>
<elementName>active</elementName>

View File

@ -0,0 +1,54 @@
/**
*
* Copyright 2021 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 static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.util.StringUtils;
import org.junit.jupiter.api.Test;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.JidTestUtil;
public class AvatarMetadataStoreTest {
@Test
public void testStoreHasAvatarAvailable() {
AvatarMetadataStore store = new MemoryAvatarMetadataStore();
EntityBareJid exist = JidTestUtil.BARE_JID_1;
EntityBareJid notExist = JidTestUtil.BARE_JID_2;
for (String itemId : getRandomIds(10)) {
assertFalse(store.hasAvatarAvailable(exist, itemId));
store.setAvatarAvailable(exist, itemId);
assertTrue(store.hasAvatarAvailable(exist, itemId));
assertFalse(store.hasAvatarAvailable(notExist, itemId));
}
}
private static List<String> getRandomIds(int len) {
List<String> ids = new ArrayList<>(len);
for (int i = 0; i < len; i++) {
ids.add(StringUtils.randomString(14));
}
return ids;
}
}

View File

@ -0,0 +1,49 @@
/**
*
* Copyright 2017 Fernando Ramirez, 2021 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 static org.junit.jupiter.api.Assertions.assertEquals;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.SmackTestUtil;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.avatar.element.DataExtension;
import org.jivesoftware.smackx.avatar.provider.DataProvider;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
public class DataExtensionTest extends SmackTestSuite {
// @formatter:off
String dataExtensionExample = "<data xmlns='urn:xmpp:avatar:data'>"
+ "qANQR1DBwU4DX7jmYZnnfe32"
+ "</data>";
// @formatter:on
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void checkDataExtensionParse(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
byte[] data = Base64.decode("qANQR1DBwU4DX7jmYZnnfe32");
DataExtension dataExtension = new DataExtension(data);
assertEquals(dataExtensionExample, dataExtension.toXML().toString());
DataExtension dataExtensionFromProvider = SmackTestUtil.parse(dataExtensionExample, DataProvider.class, parserKind);
assertEquals(Base64.encodeToString(data), Base64.encodeToString(dataExtensionFromProvider.getData()));
}
}

View File

@ -0,0 +1,214 @@
/**
*
* Copyright 2017 Fernando Ramirez, 2021 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jivesoftware.smack.test.util.SmackTestUtil;
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
import org.jivesoftware.smackx.avatar.provider.MetadataProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
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>";
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void checkMetadataExtensionParse(SmackTestUtil.XmlPullParserKind parserKind) 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);
assertEquals(metadataExtensionExample, metadataExtension.toXML().toString());
MetadataExtension metadataExtensionFromProvider = SmackTestUtil
.parse(metadataExtensionExample, MetadataProvider.class, parserKind);
assertEquals(id, metadataExtensionFromProvider.getInfoElements().get(0).getId());
assertEquals(url, metadataExtensionFromProvider.getInfoElements().get(0).getUrl());
assertEquals(bytes, metadataExtensionFromProvider.getInfoElements().get(0).getBytes().intValue());
assertEquals(type, metadataExtensionFromProvider.getInfoElements().get(0).getType());
assertEquals(pixelsHeight, metadataExtensionFromProvider.getInfoElements().get(0).getHeight().intValue());
assertEquals(pixelsWidth, metadataExtensionFromProvider.getInfoElements().get(0).getWidth().intValue());
}
@Test
public void checkEmptyMetadataExtensionParse() {
MetadataExtension metadataExtension = new MetadataExtension(null);
assertEquals(emptyMetadataExtensionExample, metadataExtension.toXML().toString());
}
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void checkSeveralInfosInMetadataExtension(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
MetadataExtension metadataExtensionFromProvider = SmackTestUtil
.parse(metadataWithSeveralInfos, MetadataProvider.class, parserKind);
MetadataInfo info1 = metadataExtensionFromProvider.getInfoElements().get(0);
MetadataInfo info2 = metadataExtensionFromProvider.getInfoElements().get(1);
MetadataInfo info3 = metadataExtensionFromProvider.getInfoElements().get(2);
assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info1.getId());
assertNull(info1.getUrl());
assertEquals(12345, info1.getBytes().intValue());
assertEquals("image/png", info1.getType());
assertEquals(64, info1.getHeight().intValue());
assertEquals(64, info1.getWidth().intValue());
assertEquals("e279f80c38f99c1e7e53e262b440993b2f7eea57", info2.getId());
assertEquals(new URL("http://avatars.example.org/happy.png"), info2.getUrl());
assertEquals(12345, info2.getBytes().intValue());
assertEquals("image/png", info2.getType());
assertEquals(64, info2.getHeight().intValue());
assertEquals(128, info2.getWidth().intValue());
assertEquals("357a8123a30844a3aa99861b6349264ba67a5694", info3.getId());
assertEquals(new URL("http://avatars.example.org/happy.gif"), info3.getUrl());
assertEquals(23456, info3.getBytes().intValue());
assertEquals("image/gif", info3.getType());
assertEquals(64, info3.getHeight().intValue());
assertEquals(64, info3.getWidth().intValue());
}
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void checkInfosAndPointersParse(SmackTestUtil.XmlPullParserKind parserKind) throws Exception {
MetadataExtension metadataExtensionFromProvider = SmackTestUtil
.parse(metadataWithInfoAndPointers, MetadataProvider.class, parserKind);
MetadataInfo info = metadataExtensionFromProvider.getInfoElements().get(0);
assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info.getId());
assertNull(info.getUrl());
assertEquals(12345, info.getBytes().intValue());
assertEquals("image/png", info.getType());
assertEquals(64, info.getHeight().intValue());
assertEquals(64, info.getWidth().intValue());
MetadataPointer pointer1 = metadataExtensionFromProvider.getPointerElements().get(0);
Map<String, Object> fields1 = pointer1.getFields();
assertEquals("http://example.com/virtualworlds", pointer1.getNamespace());
assertEquals("Ancapistan", fields1.get("game"));
assertEquals("Kropotkin", fields1.get("character"));
MetadataPointer pointer2 = metadataExtensionFromProvider.getPointerElements().get(1);
Map<String, Object> fields2 = pointer2.getFields();
assertEquals("http://sample.com/game", pointer2.getNamespace());
assertEquals("hard", fields2.get("level"));
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);
assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString());
}
}

View File

@ -0,0 +1,63 @@
/**
*
* Copyright 2021 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 static org.junit.jupiter.api.Assertions.assertThrows;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.jupiter.api.Test;
public class MetadataInfoTest {
/**
* Negative number.
*/
private static final int NEGATIVE = -13;
/**
* Greater than {@link MetadataInfo#MAX_HEIGHT} and {@link MetadataInfo#MAX_WIDTH}.
*/
private static final int TOO_LARGE = 70000;
/**
* Allowed dimension.
*/
private static final int VALID = 512;
@Test
public void throwsForNegativeBytes() {
assertThrows(IllegalArgumentException.class, () ->
new MetadataInfo("test", new URL("https://example.org/image"),
NEGATIVE, "image/png", VALID, VALID));
}
@Test
public void throwsForHeightWidthOutOfBounds() throws MalformedURLException {
URL url = new URL("https://example.org/image");
assertThrows(IllegalArgumentException.class, () ->
new MetadataInfo("test", url, 1234, "image/png", NEGATIVE, VALID));
assertThrows(IllegalArgumentException.class, () ->
new MetadataInfo("test", url, 1234, "image/png", TOO_LARGE, VALID));
assertThrows(IllegalArgumentException.class, () ->
new MetadataInfo("test", url, 1234, "image/png", VALID, NEGATIVE));
assertThrows(IllegalArgumentException.class, () ->
new MetadataInfo("test", url, 1234, "image/png", VALID, TOO_LARGE));
}
}

View File

@ -0,0 +1,127 @@
/**
*
* Copyright 2020 Paul Schaub
*
* This file is part of smack-repl.
*
* smack-repl is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.igniterealtime.smack.smackrepl;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.util.Async;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.avatar.MemoryAvatarMetadataStore;
import org.jivesoftware.smackx.avatar.MetadataInfo;
import org.jivesoftware.smackx.avatar.UserAvatarManager;
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
import org.jivesoftware.smackx.avatar.listener.AvatarListener;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.util.io.Streams;
import org.jxmpp.jid.EntityBareJid;
/**
* Connect to an XMPP account and download the avatars of all contacts.
* Shutdown with "/quit".
*/
public class Avatar {
private static final Logger LOGGER = Logger.getLogger("Avatar");
public static void main(String[] args) throws IOException, InterruptedException, XMPPException, SmackException {
if (args.length != 2) {
throw new IllegalArgumentException("Usage: java Avatar <jid> <password>");
}
XMPPTCPConnection connection = new XMPPTCPConnection(args[0], args[1]);
UserAvatarManager avatarManager = UserAvatarManager.getInstanceFor(connection);
avatarManager.setAvatarMetadataStore(new MemoryAvatarMetadataStore());
File avatarDownloadDirectory = new File(FileUtils.getTempDirectory(), "avatarTest" + StringUtils.randomString(6));
createDownloadDirectory(avatarDownloadDirectory);
avatarManager.addAvatarListener(new AvatarListener() {
@Override
public void onAvatarUpdateReceived(EntityBareJid user, MetadataExtension metadata) {
Async.go(() -> {
File userDirectory = new File(avatarDownloadDirectory, user.asUrlEncodedString());
userDirectory.mkdirs();
MetadataInfo avatarInfo = metadata.getInfoElements().get(0);
File avatarFile = new File(userDirectory, avatarInfo.getId());
try {
if (avatarInfo.getUrl() == null) {
LOGGER.log(Level.INFO, "Fetch avatar from pubsub for " + user.toString());
byte[] bytes = avatarManager.fetchAvatarFromPubSub(user, avatarInfo);
writeAvatar(avatarFile, new ByteArrayInputStream(bytes));
} else {
LOGGER.log(Level.INFO, "Fetch avatar from " + avatarInfo.getUrl().toString() + " for " + user.toString());
writeAvatar(avatarFile, avatarInfo.getUrl().openStream());
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error downloading avatar", e);
}
});
}
});
avatarManager.enable();
connection.connect().login();
Scanner input = new Scanner(System.in, StandardCharsets.UTF_8.name());
while (true) {
String line = input.nextLine();
if (line.equals("/quit")) {
connection.disconnect();
System.exit(0);
break;
}
}
}
private static void createDownloadDirectory(File avatarDownloadDirectory) throws IOException {
if (!avatarDownloadDirectory.mkdirs()) {
throw new IOException("Cannot create temp directory '" + avatarDownloadDirectory.getAbsolutePath() + "'");
} else {
LOGGER.info("Created temporary avatar download directory '" + avatarDownloadDirectory.getAbsolutePath() + "'");
}
}
private static void writeAvatar(File file, InputStream inputStream) throws IOException {
file.createNewFile();
OutputStream outputStream = new FileOutputStream(file);
Streams.pipeAll(inputStream, outputStream);
inputStream.close();
outputStream.close();
}
}