mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-27 06:22:07 +01:00
XEP-0084: User Avatar
Co-authored-by: Paul Schaub <vanitasvitae@fsfe.org>
This commit is contained in:
parent
1df0763f92
commit
0f5297c957
23 changed files with 2045 additions and 0 deletions
|
@ -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. |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue