123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720 |
- /*
- * Copyright (c) 2006-2017 DMDirc Developers
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
- package com.dmdirc.parser.xmpp;
-
- import com.dmdirc.parser.common.AwayState;
- import com.dmdirc.parser.common.BaseSocketAwareParser;
- import com.dmdirc.parser.common.ChannelJoinRequest;
- import com.dmdirc.parser.common.ChildImplementations;
- import com.dmdirc.parser.common.CompositionState;
- import com.dmdirc.parser.common.DefaultStringConverter;
- import com.dmdirc.parser.common.ParserError;
- import com.dmdirc.parser.common.QueuePriority;
- import com.dmdirc.parser.events.AwayStateEvent;
- import com.dmdirc.parser.events.ChannelSelfJoinEvent;
- import com.dmdirc.parser.events.CompositionStateChangeEvent;
- import com.dmdirc.parser.events.ConnectErrorEvent;
- import com.dmdirc.parser.events.DataInEvent;
- import com.dmdirc.parser.events.DataOutEvent;
- import com.dmdirc.parser.events.NumericEvent;
- import com.dmdirc.parser.events.OtherAwayStateEvent;
- import com.dmdirc.parser.events.PrivateActionEvent;
- import com.dmdirc.parser.events.PrivateMessageEvent;
- import com.dmdirc.parser.events.ServerReadyEvent;
- import com.dmdirc.parser.events.SocketCloseEvent;
- import com.dmdirc.parser.interfaces.ChannelInfo;
- import com.dmdirc.parser.interfaces.ClientInfo;
- import com.dmdirc.parser.interfaces.LocalClientInfo;
- import com.dmdirc.parser.interfaces.StringConverter;
-
- import java.net.URI;
- import java.time.LocalDateTime;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- import org.jivesoftware.smack.Chat;
- import org.jivesoftware.smack.ChatManagerListener;
- import org.jivesoftware.smack.ConnectionConfiguration;
- import org.jivesoftware.smack.ConnectionListener;
- import org.jivesoftware.smack.PacketListener;
- import org.jivesoftware.smack.RosterEntry;
- import org.jivesoftware.smack.RosterListener;
- import org.jivesoftware.smack.XMPPConnection;
- import org.jivesoftware.smack.XMPPException;
- import org.jivesoftware.smack.filter.PacketFilter;
- import org.jivesoftware.smack.packet.Message;
- import org.jivesoftware.smack.packet.Packet;
- import org.jivesoftware.smack.packet.Presence;
- import org.jivesoftware.smackx.ChatState;
- import org.jivesoftware.smackx.ChatStateListener;
- import org.jivesoftware.smackx.ChatStateManager;
- import org.jivesoftware.smackx.muc.MultiUserChat;
- import org.slf4j.LoggerFactory;
-
- /**
- * A parser which can understand the XMPP protocol.
- */
- @ChildImplementations({
- XmppClientInfo.class, XmppLocalClientInfo.class, XmppFakeChannel.class,
- XmppChannelClientInfo.class
- })
- public class XmppParser extends BaseSocketAwareParser {
-
- private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(XmppParser.class);
- /** Pattern to use to extract priority. */
- private static final Pattern PRIORITY_PATTERN = Pattern.compile(
- "(?i)(?:^|&)priority=([0-9]+)(?:$|&)");
- /** The connection to use. */
- private XMPPConnection connection;
- /** The state manager for the current connection. */
- private ChatStateManager stateManager;
- /** A cache of known chats. */
- private final Map<String, Chat> chats = new HashMap<>();
- /** A cache of known clients. */
- private final Map<String, XmppClientInfo> contacts = new HashMap<>();
- /** Whether or not to use a fake local channel for a buddy list replacement. */
- private final boolean useFakeChannel;
- /** The priority of this endpoint. */
- private final int priority;
- /** The fake channel to use is useFakeChannel is enabled. */
- private XmppFakeChannel fakeChannel;
-
- /**
- * Creates a new XMPP parser for the specified address.
- *
- * @param address The address to connect to
- */
- public XmppParser(final URI address) {
- super(address);
-
- if (address.getQuery() == null) {
- useFakeChannel = false;
- priority = 0;
- } else {
- final Matcher matcher = PRIORITY_PATTERN.matcher(address.getQuery());
-
- useFakeChannel = address.getQuery().matches("(?i).*(^|&)showchannel($|&).*");
- priority = matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
- }
-
- LOG.debug(
- "XMPP parser created with query string {}, parsed fake channel = {}, priority = {}",
- address.getQuery(), useFakeChannel, priority);
- }
-
- @Override
- public void disconnect(final String message) {
- super.disconnect(message);
- // TODO: Pass quit message on as presence?
- connection.disconnect();
- }
-
- @Override
- public void joinChannels(final ChannelJoinRequest... channels) {
- for (ChannelJoinRequest request : channels) {
- final MultiUserChat muc = new MultiUserChat(connection, request.getName().substring(1));
-
- try {
- if (request.getPassword() == null) {
- muc.join(getLocalClient().getNickname());
- } else {
- muc.join(getLocalClient().getNickname(), request.getPassword());
- }
-
- // TODO: Send callbacks etc
- } catch (XMPPException ex) {
- // TODO: handle
- }
- }
- }
-
- @Override
- public ChannelInfo getChannel(final String channel) {
- // TODO: Implement
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public Collection<? extends ChannelInfo> getChannels() {
- return Collections.<ChannelInfo>emptyList();
- }
-
- @Override
- public int getMaxLength(final String type, final String target) {
- return Integer.MAX_VALUE;
- }
-
- @Override
- public int getMaxLength() {
- return Integer.MAX_VALUE;
- }
-
- @Override
- public LocalClientInfo getLocalClient() {
- final String[] parts = parseHostmask(connection.getUser());
-
- // TODO: Cache this
- return new XmppLocalClientInfo(this, parts[0], parts[2], parts[1]);
- }
-
- @Override
- public XmppClientInfo getClient(final String details) {
- final String[] parts = parseHostmask(details);
-
- if (!contacts.containsKey(parts[0])) {
- contacts.put(parts[0], new XmppClientInfo(this, parts[0], parts[2], parts[1]));
- }
-
- return contacts.get(parts[0]);
- }
-
- @Override
- public void sendRawMessage(final String message) {
- // Urgh, hacky horrible rubbish. These commands should call methods.
- if (message.toUpperCase().startsWith("WHOIS ")) {
- handleWhois(message.split(" ")[1]);
- } else if (!message.isEmpty() && message.charAt(0) == '<') {
- // Looks vaguely like XML, let's send it.
- connection.sendPacket(new Packet() {
-
- @Override
- public String toXML() {
- return message;
- }
- });
- }
- }
-
- /**
- * Handles a whois request for the specified target.
- *
- * @param target The user being WHOIS'd
- */
- private void handleWhois(final String target) {
- // Urgh, hacky horrible rubbish. This should be abstracted.
- if (contacts.containsKey(target)) {
- final XmppClientInfo client = contacts.get(target);
- final String[] userParts = client.getNickname().split("@", 2);
-
- callNumericCallback(311, target, userParts[0], userParts[1], "*", client.getRealname());
-
- for (Map.Entry<String, XmppEndpoint> endpoint : client.getEndpoints().entrySet()) {
- callNumericCallback(399, target, endpoint.getKey(),
- "(" + endpoint.getValue().getPresence() + ")", "has endpoint");
- }
- } else {
- callNumericCallback(401, target, "No such contact found");
- }
-
- callNumericCallback(318, target, "End of /WHOIS.");
- }
-
- private void callNumericCallback(final int numeric, final String... args) {
- final String[] newArgs = new String[args.length + 3];
- newArgs[0] = ":xmpp.server";
- newArgs[1] = (numeric < 100 ? "0" : "") + (numeric < 10 ? "0" : "") + numeric;
- newArgs[2] = getLocalClient().getNickname();
- System.arraycopy(args, 0, newArgs, 3, args.length);
-
- getCallbackManager().publish(new NumericEvent(this, LocalDateTime.now(), numeric, newArgs));
- }
-
- @Override
- public void sendRawMessage(final String message, final QueuePriority priority) {
- sendRawMessage(message);
- }
-
- @Override
- public StringConverter getStringConverter() {
- return new DefaultStringConverter();
- }
-
- @Override
- public boolean isValidChannelName(final String name) {
- return false; // TODO: Implement
- }
-
- @Override
- public boolean compareURI(final URI uri) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public Collection<? extends ChannelJoinRequest> extractChannels(final URI uri) {
- return Collections.<ChannelJoinRequest>emptyList();
- }
-
- @Override
- public String getNetworkName() {
- return "XMPP"; // TODO
- }
-
- @Override
- public String getServerSoftware() {
- return "Unknown"; // TODO
- }
-
- @Override
- public String getServerSoftwareType() {
- return "XMPP"; // TODO
- }
-
- @Override
- public List<String> getServerInformationLines() {
- return Collections.emptyList(); // TODO
- }
-
- @Override
- public int getMaxTopicLength() {
- return 0; // TODO
- }
-
- @Override
- public String getBooleanChannelModes() {
- return ""; // TODO
- }
-
- @Override
- public String getListChannelModes() {
- return ""; // TODO
- }
-
- @Override
- public int getMaxListModes(final char mode) {
- return 0; // TODO
- }
-
- @Override
- public boolean isUserSettable(final char mode) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public String getParameterChannelModes() {
- return ""; // TODO
- }
-
- @Override
- public String getDoubleParameterChannelModes() {
- return ""; // TODO
- }
-
- @Override
- public String getUserModes() {
- return ""; // TODO
- }
-
- @Override
- public String getChannelUserModes() {
- return ""; // TODO
- }
-
- @Override
- public String getChannelPrefixes() {
- return "#";
- }
-
- @Override
- public long getServerLatency() {
- return 1000L; // TODO
- }
-
- @Override
- public void sendCTCP(final String target, final String type, final String message) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void sendCTCPReply(final String target, final String type, final String message) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void sendMessage(final String target, final String message) {
- if (!chats.containsKey(target)) {
- LOG.debug("Creating new chat for {}", target);
- chats.put(target, connection.getChatManager().createChat(target,
- new MessageListenerImpl()));
- }
-
- try {
- chats.get(target).sendMessage(message);
- } catch (XMPPException ex) {
- // TODO: Handle this
- }
- }
-
- @Override
- public void sendNotice(final String target, final String message) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void sendAction(final String target, final String message) {
- sendMessage(target, "/me " + message);
- }
-
- @Override
- public void sendInvite(final String channel, final String user) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void sendWhois(final String nickname) {
- // TODO: Implement this
- }
-
- @Override
- public String getLastLine() {
- return "TODO: Implement me";
- }
-
- @Override
- public String[] parseHostmask(final String hostmask) {
- return new XmppProtocolDescription().parseHostmask(hostmask);
- }
-
- @Override
- public long getPingTime() {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void run() {
- if (getURI().getUserInfo() == null || !getURI().getUserInfo().contains(":")) {
- getCallbackManager().publish(new ConnectErrorEvent(this, LocalDateTime.now(),
- new ParserError(ParserError.ERROR_USER,
- "User name and password must be specified in URI", "")));
- return;
- }
- final String[] userInfoParts = getURI().getUserInfo().split(":", 2);
- final String[] userParts = userInfoParts[0].split("@", 2);
-
- final ConnectionConfiguration config = new ConnectionConfiguration(getURI().getHost(),
- getURI().getPort(), userParts[0]);
- config.setSecurityMode(getURI().getScheme().equalsIgnoreCase("xmpps")
- ? ConnectionConfiguration.SecurityMode.required
- : ConnectionConfiguration.SecurityMode.disabled);
- config.setSASLAuthenticationEnabled(true);
- config.setReconnectionAllowed(false);
- config.setRosterLoadedAtLogin(true);
- config.setSocketFactory(getSocketFactory());
- connection = new FixedXmppConnection(config);
-
- try {
- connection.connect();
-
- connection.addConnectionListener(new ConnectionListenerImpl());
- connection.addPacketListener(new PacketListenerImpl(false),
- new AcceptAllPacketFilter());
- connection.addPacketSendingListener(new PacketListenerImpl(true),
- new AcceptAllPacketFilter());
- connection.getChatManager().addChatListener(new ChatManagerListenerImpl());
-
- try {
- connection.login(userInfoParts[0], userInfoParts[1], "DMDirc.");
- } catch (XMPPException ex) {
- getCallbackManager().publish(new ConnectErrorEvent(this, LocalDateTime.now(),
- new ParserError(ParserError.ERROR_USER, ex.getMessage(), "")));
- return;
- }
-
- connection.sendPacket(new Presence(Presence.Type.available, null, priority,
- Presence.Mode.available));
- connection.getRoster().addRosterListener(new RosterListenerImpl());
-
- stateManager = ChatStateManager.getInstance(connection);
-
- setServerName(connection.getServiceName());
-
- getCallbackManager().publish(new ServerReadyEvent(this, LocalDateTime.now()));
-
- for (RosterEntry contact : connection.getRoster().getEntries()) {
- getClient(contact.getUser()).setRosterEntry(contact);
- }
-
- if (useFakeChannel) {
- fakeChannel = new XmppFakeChannel(this, "&contacts");
- getCallbackManager().publish(new ChannelSelfJoinEvent(null, null, fakeChannel));
- fakeChannel.updateContacts(contacts.values());
-
- contacts.values().stream().filter(XmppClientInfo::isAway).forEach(client ->
- getCallbackManager().publish(
- new OtherAwayStateEvent(this, LocalDateTime.now(), client,
- AwayState.UNKNOWN, AwayState.AWAY)));
- }
- } catch (XMPPException ex) {
- LOG.debug("Go an XMPP exception", ex);
-
- connection = null;
-
- final ParserError error = new ParserError(ParserError.ERROR_ERROR, "Unable to connect",
- "");
-
- if (ex.getWrappedThrowable() instanceof Exception) {
- // Pass along the underlying exception instead of an XMPP
- // specific one
- error.setException((Exception) ex.getWrappedThrowable());
- } else {
- error.setException(ex);
- }
-
- getCallbackManager().publish(new ConnectErrorEvent(this, LocalDateTime.now(), error));
- }
- }
-
- /**
- * Handles a client's away state changing.
- *
- * @param client The client whose state is changing
- * @param isBack True if the client is coming back, false if they're going away
- */
- public void handleAwayStateChange(final ClientInfo client, final boolean isBack) {
- LOG.debug("Handling away state change for {} to {}", client.getNickname(), isBack);
-
- if (useFakeChannel) {
- getCallbackManager().publish(new OtherAwayStateEvent(
- this, LocalDateTime.now(), client, isBack ? AwayState.AWAY : AwayState.HERE,
- isBack ? AwayState.HERE : AwayState.AWAY));
- }
- }
-
- /**
- * Marks the local user as away with the specified reason.
- *
- * @param reason The away reason
- */
- public void setAway(final String reason) {
- connection.sendPacket(new Presence(Presence.Type.available, reason,
- priority, Presence.Mode.away));
-
- getCallbackManager().publish(
- new AwayStateEvent(this, LocalDateTime.now(), AwayState.HERE, AwayState.AWAY,
- reason));
- }
-
- /**
- * Marks the local user as back.
- */
- public void setBack() {
- connection.sendPacket(new Presence(Presence.Type.available, null,
- priority, Presence.Mode.available));
-
- getCallbackManager().publish(
- new AwayStateEvent(this, LocalDateTime.now(), AwayState.AWAY, AwayState.HERE,
- null));
- }
-
- @Override
- public void setCompositionState(final String host, final CompositionState state) {
- LOG.debug("Setting composition state for {} to {}", host, state);
-
- final Chat chat = chats.get(parseHostmask(host)[0]);
-
- final ChatState newState;
-
- switch (state) {
- case ENTERED_TEXT:
- newState = ChatState.paused;
- break;
- case TYPING:
- newState = ChatState.composing;
- break;
- case IDLE:
- default:
- newState = ChatState.active;
- break;
- }
-
- if (chat != null && stateManager != null) {
- try {
- stateManager.setCurrentState(newState, chat);
- } catch (XMPPException ex) {
- // Can't set chat state... Oh well?
- LOG.info("Couldn't set composition state", ex);
- }
- }
- }
-
- @Override
- public void requestGroupList(final String searchTerms) {
- // Do nothing
- }
-
- private class ConnectionListenerImpl implements ConnectionListener {
-
- @Override
- public void connectionClosed() {
- getCallbackManager().publish(new SocketCloseEvent(XmppParser.this,
- LocalDateTime.now()));
- }
-
- @Override
- public void connectionClosedOnError(final Exception excptn) {
- // TODO: Handle exception
- getCallbackManager().publish(new SocketCloseEvent(XmppParser.this,
- LocalDateTime.now()));
- }
-
- @Override
- public void reconnectingIn(final int i) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void reconnectionSuccessful() {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public void reconnectionFailed(final Exception excptn) {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- }
-
- private class RosterListenerImpl implements RosterListener {
-
- @Override
- public void entriesAdded(final Collection<String> clctn) {
- // Do nothing, yet
- }
-
- @Override
- public void entriesUpdated(final Collection<String> clctn) {
- // Do nothing, yet
- }
-
- @Override
- public void entriesDeleted(final Collection<String> clctn) {
- // Do nothing, yet
- }
-
- @Override
- public void presenceChanged(final Presence prsnc) {
- getClient(prsnc.getFrom()).setPresence(prsnc);
- }
-
- }
-
- private class ChatManagerListenerImpl implements ChatManagerListener {
-
- @Override
- public void chatCreated(final Chat chat, final boolean bln) {
- if (!bln) {
- // Only add chats that weren't created locally
- chats.put(parseHostmask(chat.getParticipant())[0], chat);
- chat.addMessageListener(new MessageListenerImpl());
- }
- }
-
- }
-
- private class MessageListenerImpl implements ChatStateListener {
-
- @Override
- public void processMessage(final Chat chat, final Message msg) {
- if (msg.getType() == Message.Type.error) {
- getCallbackManager().publish(new NumericEvent(XmppParser.this, LocalDateTime.now(),
- 404,
- new String[]{":xmpp", "404", getLocalClient().getNickname(), msg.getFrom(),
- "Cannot send message: " + msg.getError().toString()}));
- return;
- }
-
- if (msg.getBody() != null) {
- if (msg.getBody().startsWith("/me ")) {
- getCallbackManager().publish(new PrivateActionEvent(XmppParser.this,
- LocalDateTime.now(),
- msg.getBody().substring(4), msg.getFrom()));
- } else {
- getCallbackManager().publish(
- new PrivateMessageEvent(XmppParser.this,
- LocalDateTime.now(), msg.getBody(),
- msg.getFrom()));
- }
- }
- }
-
- @Override
- public void stateChanged(final Chat chat, final ChatState cs) {
- final CompositionState state;
-
- switch (cs) {
- case paused:
- state = CompositionState.ENTERED_TEXT;
- break;
- case composing:
- state = CompositionState.TYPING;
- break;
- case active:
- case gone:
- case inactive:
- default:
- state = CompositionState.IDLE;
- break;
- }
-
- getCallbackManager().publish(
- new CompositionStateChangeEvent(XmppParser.this, LocalDateTime.now(), state,
- chat.getParticipant()));
- }
-
- }
-
- private class PacketListenerImpl implements PacketListener {
-
- private final boolean dataOut;
-
- public PacketListenerImpl(final boolean dataOut) {
- this.dataOut = dataOut;
- }
-
- @Override
- public void processPacket(final Packet packet) {
- if (dataOut) {
- getCallbackManager().publish(
- new DataOutEvent(XmppParser.this, LocalDateTime.now(), packet.toXML()));
- } else {
- getCallbackManager().publish(
- new DataInEvent(XmppParser.this, LocalDateTime.now(), packet.toXML()));
- }
- }
-
- }
-
- private static class AcceptAllPacketFilter implements PacketFilter {
-
- @Override
- public boolean accept(final Packet packet) {
- return true;
- }
-
- }
-
- }
|