1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-06-26 05:14:49 +02:00
Smack/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/ChatManager.java
Florian Schmaus d07d46bbdc [chat2] Fix resource unlock condition
If the resource of the updated presence is equal to the currently
locked resource, then no further action is required.

Thanks to Pedro Oliveira for reporting this.

Reported-by: Pedro Oliveira <kanytu@gmail.com>
2021-03-25 08:22:12 +01:00

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