mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-09-27 18:19:33 +02:00
e2d206e741
add deprecate addStanzaInterceptor().
271 lines
9.6 KiB
Java
271 lines
9.6 KiB
Java
/**
|
|
*
|
|
* Copyright 2017-2019 Florian Schmaus.
|
|
*
|
|
* 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.smack.chat2;
|
|
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.WeakHashMap;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.CopyOnWriteArraySet;
|
|
|
|
import org.jivesoftware.smack.AsyncButOrdered;
|
|
import org.jivesoftware.smack.Manager;
|
|
import org.jivesoftware.smack.StanzaListener;
|
|
import org.jivesoftware.smack.XMPPConnection;
|
|
import org.jivesoftware.smack.filter.AndFilter;
|
|
import org.jivesoftware.smack.filter.FromTypeFilter;
|
|
import org.jivesoftware.smack.filter.MessageTypeFilter;
|
|
import org.jivesoftware.smack.filter.MessageWithBodiesFilter;
|
|
import org.jivesoftware.smack.filter.OrFilter;
|
|
import org.jivesoftware.smack.filter.StanzaExtensionFilter;
|
|
import org.jivesoftware.smack.filter.StanzaFilter;
|
|
import org.jivesoftware.smack.filter.ToTypeFilter;
|
|
import org.jivesoftware.smack.packet.Message;
|
|
import org.jivesoftware.smack.packet.MessageView;
|
|
import org.jivesoftware.smack.packet.Presence;
|
|
import org.jivesoftware.smack.packet.Stanza;
|
|
import org.jivesoftware.smack.roster.AbstractRosterListener;
|
|
import org.jivesoftware.smack.roster.Roster;
|
|
|
|
import org.jivesoftware.smackx.xhtmlim.packet.XHTMLExtension;
|
|
|
|
import org.jxmpp.jid.EntityBareJid;
|
|
import org.jxmpp.jid.EntityFullJid;
|
|
import org.jxmpp.jid.Jid;
|
|
|
|
/**
|
|
* A chat manager for 1:1 XMPP instant messaging chats.
|
|
* <p>
|
|
* This manager and the according {@link Chat} API implement "Resource Locking" (XEP-0296). Support for Carbon Copies
|
|
* (XEP-0280) will be added once the XEP has progressed from experimental.
|
|
* </p>
|
|
*
|
|
* @see <a href="https://xmpp.org/extensions/xep-0296.html">XEP-0296: Best Practices for Resource Locking</a>
|
|
*/
|
|
public final class ChatManager extends Manager {
|
|
|
|
private static final Map<XMPPConnection, ChatManager> INSTANCES = new WeakHashMap<>();
|
|
|
|
public static synchronized ChatManager getInstanceFor(XMPPConnection connection) {
|
|
ChatManager chatManager = INSTANCES.get(connection);
|
|
if (chatManager == null) {
|
|
chatManager = new ChatManager(connection);
|
|
INSTANCES.put(connection, chatManager);
|
|
}
|
|
return chatManager;
|
|
}
|
|
|
|
// @FORMATTER:OFF
|
|
private static final StanzaFilter MESSAGE_FILTER = new AndFilter(
|
|
MessageTypeFilter.NORMAL_OR_CHAT,
|
|
new OrFilter(MessageWithBodiesFilter.INSTANCE, new StanzaExtensionFilter(XHTMLExtension.ELEMENT, XHTMLExtension.NAMESPACE))
|
|
);
|
|
|
|
private static final StanzaFilter OUTGOING_MESSAGE_FILTER = new AndFilter(
|
|
MESSAGE_FILTER,
|
|
ToTypeFilter.ENTITY_FULL_OR_BARE_JID
|
|
);
|
|
|
|
private static final StanzaFilter INCOMING_MESSAGE_FILTER = new AndFilter(
|
|
MESSAGE_FILTER,
|
|
FromTypeFilter.ENTITY_FULL_JID
|
|
);
|
|
// @FORMATTER:ON
|
|
|
|
private final Map<EntityBareJid, Chat> chats = new ConcurrentHashMap<>();
|
|
|
|
private final Set<IncomingChatMessageListener> incomingListeners = new CopyOnWriteArraySet<>();
|
|
|
|
private final Set<OutgoingChatMessageListener> outgoingListeners = new CopyOnWriteArraySet<>();
|
|
|
|
private final AsyncButOrdered<Chat> asyncButOrdered = new AsyncButOrdered<>();
|
|
|
|
private boolean xhtmlIm;
|
|
|
|
private ChatManager(final XMPPConnection connection) {
|
|
super(connection);
|
|
connection.addSyncStanzaListener(new StanzaListener() {
|
|
@Override
|
|
public void processStanza(Stanza stanza) {
|
|
final Message message = (Message) stanza;
|
|
if (!shouldAcceptMessage(message)) {
|
|
return;
|
|
}
|
|
|
|
final Jid from = message.getFrom();
|
|
final EntityFullJid fullFrom = from.asEntityFullJidOrThrow();
|
|
final EntityBareJid bareFrom = fullFrom.asEntityBareJid();
|
|
final Chat chat = chatWith(bareFrom);
|
|
chat.lockedResource = fullFrom;
|
|
|
|
asyncButOrdered.performAsyncButOrdered(chat, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
for (IncomingChatMessageListener listener : incomingListeners) {
|
|
listener.newIncomingMessage(bareFrom, message, chat);
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
}, INCOMING_MESSAGE_FILTER);
|
|
|
|
connection.addMessageInterceptor(messageBuilder -> {
|
|
if (!shouldAcceptMessage(messageBuilder)) {
|
|
return;
|
|
}
|
|
|
|
final EntityBareJid to = messageBuilder.getTo().asEntityBareJidOrThrow();
|
|
final Chat chat = chatWith(to);
|
|
|
|
for (OutgoingChatMessageListener listener : outgoingListeners) {
|
|
listener.newOutgoingMessage(to, messageBuilder, chat);
|
|
}
|
|
}, m -> {
|
|
return OUTGOING_MESSAGE_FILTER.accept(m);
|
|
});
|
|
|
|
Roster roster = Roster.getInstanceFor(connection);
|
|
roster.addRosterListener(new AbstractRosterListener() {
|
|
@Override
|
|
public void presenceChanged(Presence presence) {
|
|
final Jid from = presence.getFrom();
|
|
final EntityBareJid bareFrom = from.asEntityBareJidIfPossible();
|
|
if (bareFrom == null) {
|
|
return;
|
|
}
|
|
|
|
final Chat chat = chats.get(bareFrom);
|
|
if (chat == null) {
|
|
return;
|
|
}
|
|
|
|
if (chat.lockedResource == null) {
|
|
// According to XEP-0296, no action is required for resource locking upon receiving a presence if no
|
|
// resource is currently locked.
|
|
return;
|
|
}
|
|
|
|
final EntityFullJid fullFrom = from.asEntityFullJidIfPossible();
|
|
if (!chat.lockedResource.equals(fullFrom)) {
|
|
return;
|
|
}
|
|
|
|
if (chat.lastPresenceOfLockedResource == null) {
|
|
// We have no last known presence from the locked resource.
|
|
chat.lastPresenceOfLockedResource = presence;
|
|
return;
|
|
}
|
|
|
|
if (chat.lastPresenceOfLockedResource.getMode() != presence.getMode()
|
|
|| chat.lastPresenceOfLockedResource.getType() != presence.getType()) {
|
|
chat.unlockResource();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private boolean shouldAcceptMessage(MessageView message) {
|
|
if (message.hasExtension(Message.Body.QNAME)) {
|
|
return true;
|
|
}
|
|
|
|
// Message has no XMPP-IM bodies, abort here if xhtmlIm is not enabled.
|
|
if (!xhtmlIm) {
|
|
return false;
|
|
}
|
|
|
|
XHTMLExtension xhtmlExtension = XHTMLExtension.from(message);
|
|
if (xhtmlExtension == null) {
|
|
// Message has no XHTML-IM extension, abort.
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add a new listener for incoming chat messages.
|
|
*
|
|
* @param listener the listener to add.
|
|
* @return <code>true</code> if the listener was not already added.
|
|
*/
|
|
public boolean addIncomingListener(IncomingChatMessageListener listener) {
|
|
return incomingListeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove an incoming chat message listener.
|
|
*
|
|
* @param listener the listener to remove.
|
|
* @return <code>true</code> if the listener was active and got removed.
|
|
*/
|
|
public boolean removeIncomingListener(IncomingChatMessageListener listener) {
|
|
return incomingListeners.remove(listener);
|
|
}
|
|
|
|
/**
|
|
* Add a new listener for outgoing chat messages.
|
|
*
|
|
* @param listener the listener to add.
|
|
* @return <code>true</code> if the listener was not already added.
|
|
*/
|
|
public boolean addOutgoingListener(OutgoingChatMessageListener listener) {
|
|
return outgoingListeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove an outgoing chat message listener.
|
|
*
|
|
* @param listener the listener to remove.
|
|
* @return <code>true</code> if the listener was active and got removed.
|
|
*/
|
|
public boolean removeOutgoingListener(OutgoingChatMessageListener listener) {
|
|
return outgoingListeners.remove(listener);
|
|
}
|
|
|
|
/**
|
|
* Start a new or retrieve the existing chat with <code>jid</code>.
|
|
*
|
|
* @param jid the XMPP address of the other entity to chat with.
|
|
* @return the Chat API for the given XMPP address.
|
|
*/
|
|
public Chat chatWith(EntityBareJid jid) {
|
|
Chat chat = chats.get(jid);
|
|
if (chat == null) {
|
|
synchronized (chats) {
|
|
// Double-checked locking.
|
|
chat = chats.get(jid);
|
|
if (chat != null) {
|
|
return chat;
|
|
}
|
|
chat = new Chat(connection(), jid);
|
|
chats.put(jid, chat);
|
|
}
|
|
}
|
|
return chat;
|
|
}
|
|
|
|
/**
|
|
* Also notify about messages containing XHTML-IM.
|
|
*
|
|
* @param xhtmlIm TODO javadoc me please
|
|
*/
|
|
public void setXhmtlImEnabled(boolean xhtmlIm) {
|
|
this.xhtmlIm = xhtmlIm;
|
|
}
|
|
}
|