From dcf1b9e5e4312cfd685c1f3cb0f819e7ac64be75 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 31 Dec 2018 01:35:11 +0300 Subject: XMPP cleanup --- src/main/java/com/juick/server/XMPPConnection.java | 672 -------------------- src/main/java/com/juick/server/XMPPManager.java | 678 +++++++++++++++++++++ .../com/juick/server/configuration/XMPPConfig.java | 23 +- .../juick/server/xmpp/s2s/BasicXmppSession.java | 68 --- .../com/juick/server/xmpp/s2s/StanzaListener.java | 28 - .../java/com/juick/server/tests/ServerTests.java | 8 - 6 files changed, 681 insertions(+), 796 deletions(-) delete mode 100644 src/main/java/com/juick/server/XMPPConnection.java create mode 100644 src/main/java/com/juick/server/XMPPManager.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java diff --git a/src/main/java/com/juick/server/XMPPConnection.java b/src/main/java/com/juick/server/XMPPConnection.java deleted file mode 100644 index c4816478..00000000 --- a/src/main/java/com/juick/server/XMPPConnection.java +++ /dev/null @@ -1,672 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.server.www.WebApp; -import com.juick.service.component.*; -import com.juick.model.CommandResult; -import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.server.xmpp.s2s.BasicXmppSession; -import com.juick.service.*; -import com.juick.util.MessageUtils; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.XmppSession; -import rocks.xmpp.core.stanza.AbstractIQHandler; -import rocks.xmpp.core.stanza.model.*; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.client.ClientPresence; -import rocks.xmpp.core.stanza.model.errors.Condition; -import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; -import rocks.xmpp.extensions.component.accept.ExternalComponent; -import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; -import rocks.xmpp.extensions.disco.model.info.Identity; -import rocks.xmpp.extensions.filetransfer.FileTransfer; -import rocks.xmpp.extensions.filetransfer.FileTransferManager; -import rocks.xmpp.extensions.nick.model.Nickname; -import rocks.xmpp.extensions.oob.model.x.OobX; -import rocks.xmpp.extensions.ping.PingManager; -import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; -import rocks.xmpp.extensions.vcard.temp.model.VCard; -import rocks.xmpp.extensions.version.SoftwareVersionManager; -import rocks.xmpp.extensions.version.model.SoftwareVersion; -import rocks.xmpp.util.XmppUtils; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.inject.Inject; -import javax.xml.bind.JAXBException; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import java.io.IOException; -import java.io.StringWriter; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -/** - * @author ugnich - */ -public class XMPPConnection implements NotificationListener { - - private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); - - private ExternalComponent router; - @Inject - private CommandsManager commandsManager; - @Value("${xmppbot_jid:juick@localhost}") - private Jid jid; - @Value("${hostname:localhost}") - private String componentName; - @Value("${component_port:5347}") - private int componentPort; - @Value("${component_host:localhost}") - private String componentHost; - @Value("${xmpp_password:secret}") - private String password; - @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") - private String tmpDir; - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private BasicXmppSession session; - @Inject - private ExecutorService executorService; - @Value("${service_user:juick}") - private String serviceUsername; - @Inject - private WebApp webApp; - - private User serviceUser; - - @PostConstruct - public void init() { - logger.info("stream router start connecting to {}", componentPort); - router = ExternalComponent.create(componentName, password, session.getConfiguration(), componentHost, componentPort); - ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class); - serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); - EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class); - entityCapabilitiesManager.setNode("https://juick.com/caps"); - MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = router.getManager(MessageDeliveryReceiptsManager.class); - messageDeliveryReceiptsManager.setEnabled(true); - PingManager pingManager = router.getManager(PingManager.class); - pingManager.setEnabled(true); - SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); - softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", - System.getProperty("os.name", "generic"))); - VCard vCard = new VCard(); - vCard.setFormattedName("Juick"); - vCard.setBirthday(LocalDate.of(2008, 10, 22)); - try { - vCard.setUrl(new URL("http://juick.com/")); - vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray( - getClass().getClassLoader().getResource("juick.png")))); - } catch (MalformedURLException e) { - logger.error("invalid url", e); - } catch (IOException e) { - logger.warn("invalid resource", e); - } - router.addIQHandler(MessageQuery.class, iq -> { - Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); - warningMessage.setFrom(jid); - warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); - router.send(warningMessage); - return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); - }); - router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { - @Override - protected IQ processRequest(IQ iq) { - if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) - || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { - return iq.createResult(vCard); - } - User user = userService.getUserByName(iq.getTo().getLocal()); - if (!user.isAnonymous()) { - User info = userService.getUserInfo(user); - VCard userVCard = new VCard(); - userVCard.setFormattedName(info.getFullName()); - userVCard.setNickname(user.getName()); - try { - userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); - if (info.getUrl() != null) { - userVCard.setUrl(new URL(info.getUrl())); - } - } catch (MalformedURLException e) { - logger.warn("url exception", e); - } - return iq.createResult(userVCard); - } - return iq.createError(Condition.BAD_REQUEST); - } - }); - router.addInboundMessageListener(e -> { - ClientMessage result = incomingMessage(e.getMessage()); - if (result != null) { - router.send(result); - } - }); - router.addInboundIQListener(e -> { - IQ iq = e.getIQ(); - Jid jid = iq.getTo(); - if (!jid.getDomain().equals(this.jid.getDomain())) { - router.send(iq); - } - }); - FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); - fileTransferManager.addFileTransferOfferListener(e -> { - try { - List allowedTypes = new ArrayList() {{ - add("png"); - add("jpg"); - }}; - String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); - String targetFilename = String.format("%s.%s", - DigestUtils.md5Hex(String.format("%s-%s", - e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); - if (allowedTypes.contains(attachmentExtension)) { - Path filePath = Paths.get(tmpDir, targetFilename); - FileTransfer ft = e.accept(filePath).get(); - ft.addFileTransferStatusListener(st -> { - logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); - if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { - logger.info("transfer completed"); - try { - Jid initiator = e.getInitiator(); - ClientMessage result = incomingMessageJuick( - userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, - jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); - if (result != null) { - router.send(result); - } - } catch (Exception e1) { - logger.error("ft error", e1); - } - - } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { - logger.info("transfer failed", ft.getException()); - Message msg = new Message(); - msg.setType(Message.Type.CHAT); - msg.setFrom(jid); - msg.setTo(e.getInitiator()); - msg.setBody("File transfer failed, please report to us"); - router.sendMessage(msg); - } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { - logger.info("transfer cancelled"); - } - }); - ft.transfer(); - logger.info("transfer started"); - } else { - e.reject(); - logger.info("transfer rejected"); - } - } catch (IOException | InterruptedException | ExecutionException e1) { - logger.error("ft error", e1); - } - }); - router.addConnectionListener(event -> { - if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { - logger.info("component connected"); - } - }); - router.addSessionStatusListener(event -> { - logger.info("event: " + event.getStatus(), event.getThrowable()); - if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { - logger.info("Authenticated, broadcasting..."); - broadcastPresence(null); - } - }); - router.addInboundPresenceListener(event -> { - incomingPresence(event.getPresence()); - }); - executorService.submit(() -> { - try { - router.connect(); - } catch (XmppException e) { - logger.warn("xmpp exception", e); - } - }); - serviceUser = userService.getUserByName(serviceUsername); - } - - private String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { - StringWriter stanzaWriter = new StringWriter(); - XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( - router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); - router.createMarshaller().marshal(stanza, xmppStreamWriter); - xmppStreamWriter.flush(); - xmppStreamWriter.close(); - return stanzaWriter.toString(); - } - - private void sendJuickMessage(com.juick.Message jmsg, List users) { - List jids = new ArrayList<>(); - - for (User user : users) { - jids.addAll(userService.getJIDsbyUID(user.getUid())); - } - com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - for (String jid : jids) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - - public void sendJuickComment(com.juick.Message jmsg, List users) { - String replyQuote; - String replyTo; - - com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) - : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - replyTo = replyMessage.getUser().getName(); - com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); - - String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; - String attachmentUrl = MessageUtils.attachmentUrl(fullReply); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.addExtension(jmsg); - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processMessageEvent(MessageEvent event) { - com.juick.Message msg = event.getMessage(); - List subscribers = event.getUsers(); - if (msg.isService()) { - return; - } - if (MessageUtils.isPM(msg)) { - userService.getJIDsbyUID(msg.getTo().getUid()) - .forEach(userJid -> { - Message mm = new Message(); - mm.setTo(Jid.of(userJid)); - mm.setType(Message.Type.CHAT); - boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); - if (inroster) { - mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); - mm.setBody(msg.getText()); - } else { - mm.setFrom(jid); - mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); - } - router.send(ClientMessage.from(mm)); - }); - } else if (MessageUtils.isReply(msg)) { - sendJuickComment(msg, subscribers); - } - else { - sendJuickMessage(msg, subscribers); - } - } - - private ClientMessage makeReply(Jid jidTo, String txt) { - Message reply = new Message(); - reply.setFrom(jid); - reply.setTo(jidTo); - reply.setType(Message.Type.CHAT); - reply.setBody(txt); - return ClientMessage.from(reply); - } - - @Override - public void processSubscribeEvent(SubscribeEvent subscribeEvent) { - - } - - @Override - public void processLikeEvent(LikeEvent likeEvent) { - List users = likeEvent.getSubscribers(); - com.juick.Message jmsg = likeEvent.getMessage(); - User liker = likeEvent.getUser(); - - if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { - userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { - Message xmppMessage = new Message(); - xmppMessage.setFrom(jid); - xmppMessage.setTo(Jid.of(authorJid)); - xmppMessage.setType(Message.Type.CHAT); - xmppMessage.addExtension(jmsg); - xmppMessage.setBody(String.format("%s recommended your post #%d. %s", - liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); - router.send(ClientMessage.from(xmppMessage)); - }); - } - - String txt = "Recommended by @" + liker.getName() + ":\n"; - txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(jmsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid(); - if (jmsg.getReplies() > 0) { - if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { - txt += " (" + jmsg.getReplies() + " reply)"; - } else { - txt += " (" + jmsg.getReplies() + " replies)"; - } - } - txt += " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - userService.getJIDsbyUID(pingEvent.getPinger().getUid()) - .forEach(userJid -> { - Presence p = new Presence(Jid.of(userJid)); - p.setFrom(jid); - p.setPriority((byte) 10); - router.send(ClientPresence.from(p)); - }); - } - - @Override - public void processMessageReadEvent(MessageReadEvent messageReadEvent) { - - } - - @Override - public void processTopEvent(TopEvent topEvent) { - com.juick.Message message = topEvent.getMessage(); - try { - commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); - } catch (Exception e) { - logger.warn("XMPP error", e); - } - } - - private void incomingPresence(Presence p) { - final String username = p.getTo().getLocal(); - final boolean toJuick = username.equals(jid.getLocal()); - - if (p.getType() == null) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().asBareJid()); - reply.setTo(p.getFrom().asBareJid()); - reply.setType(Presence.Type.UNSUBSCRIBE); - router.send(ClientPresence.from(reply)); - } else if (p.getType().equals(Presence.Type.PROBE)) { - int uid_to = 0; - if (!toJuick) { - uid_to = userService.getUIDbyName(username); - } else { - User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); - if (visitor != null) { - userService.updateLastSeen(visitor); - } - } - - if (toJuick || uid_to > 0) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().withResource(jid.getResource())); - reply.setTo(p.getFrom()); - reply.setPriority((byte)10); - if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { - reply.setStatus("Send ON to enable notifications"); - } - router.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - router.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { - boolean canSubscribe = false; - if (toJuick) { - canSubscribe = true; - } else { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - canSubscribe = true; - } - } - if (canSubscribe) { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.SUBSCRIBED); - router.send(ClientPresence.from(reply)); - - reply.setFrom(reply.getFrom().withResource(jid.getResource())); - reply.setPriority((byte) 10); - reply.setType(null); - router.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - router.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { - if (!toJuick) { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - } - } - - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.UNSUBSCRIBED); - router.send(ClientPresence.from(reply)); - } - } - - public ClientMessage incomingMessage(Message msg) { - ClientMessage result = null; - if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { - StanzaError error = msg.getError(); - if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { - // offline query is full, deactivating this jid - if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { - logger.info("{} is inactive now", msg.getFrom()); - return null; - } - } - return null; - } - Jid to = msg.getTo(); - if (to.getDomain().equals(router.getDomain().toEscapedString()) || to.equals(this.jid)) { - User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); - if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { - String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); - return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); - } - URI attachment = URI.create(StringUtils.EMPTY); - OobX oobX = msg.getExtension(OobX.class); - if (oobX != null) { - attachment = oobX.getUri(); - } - try { - return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); - } catch (Exception e1) { - logger.warn("message exception", e1); - } - } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain()) - || to.getDomain().endsWith("." + jid.getDomain()))) { - if (logger.isInfoEnabled()) { - try { - logger.info("unhandled message: {}", stanzaToString(msg)); - } catch (JAXBException | XMLStreamException ex) { - logger.error("JAXB exception", ex); - } - } - } else { - return ClientMessage.from(msg); - } - return result; - } - private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { - if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { - return null; - } - - messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); - - int commandlen = command.length(); - - // COMPATIBILITY - if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { - command = command.substring(3); - } - - if (!jid.getLocal().equals(to)) { - // PM - if (!StringUtils.isEmpty(command)) { - commandsManager.commandPM(user_from, null, to, command); - return null; - } - } - - try { - CommandResult result = commandsManager.processCommand(user_from, command, attachment); - if (StringUtils.isNotBlank(result.getText())) { - return makeReply(from, result.getText()); - } - } catch (Exception e) { - logger.warn("xmpp command exception", e); - return makeReply(from, "Error processing command"); - } - return null; - } - - private void broadcastPresence(Presence.Type type) { - Presence presence = new Presence(); - presence.setFrom(jid); - if (type != null) { - presence.setType(type); - } - userService.getActiveJIDs().forEach(j -> { - try { - presence.setTo(Jid.of(j)); - router.send(ClientPresence.from(presence)); - } catch (IllegalArgumentException ex) { - logger.warn("Invalid jid: {}", j, ex); - } - }); - } - - @PreDestroy - public void close() throws Exception { - broadcastPresence(Presence.Type.UNAVAILABLE); - if (router != null) { - router.close(); - } - } - - public ExternalComponent getRouter() { - return router; - } -} diff --git a/src/main/java/com/juick/server/XMPPManager.java b/src/main/java/com/juick/server/XMPPManager.java new file mode 100644 index 00000000..fb8f9711 --- /dev/null +++ b/src/main/java/com/juick/server/XMPPManager.java @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server; + +import com.juick.User; +import com.juick.formatters.PlainTextFormatter; +import com.juick.server.www.WebApp; +import com.juick.service.component.*; +import com.juick.model.CommandResult; +import com.juick.server.xmpp.iq.MessageQuery; +import com.juick.service.*; +import com.juick.util.MessageUtils; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSession; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.session.debug.LogbackDebugger; +import rocks.xmpp.core.stanza.AbstractIQHandler; +import rocks.xmpp.core.stanza.model.*; +import rocks.xmpp.core.stanza.model.client.ClientMessage; +import rocks.xmpp.core.stanza.model.client.ClientPresence; +import rocks.xmpp.core.stanza.model.errors.Condition; +import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; +import rocks.xmpp.extensions.component.accept.ExternalComponent; +import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; +import rocks.xmpp.extensions.disco.model.info.Identity; +import rocks.xmpp.extensions.filetransfer.FileTransfer; +import rocks.xmpp.extensions.filetransfer.FileTransferManager; +import rocks.xmpp.extensions.nick.model.Nickname; +import rocks.xmpp.extensions.oob.model.x.OobX; +import rocks.xmpp.extensions.ping.PingManager; +import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; +import rocks.xmpp.extensions.vcard.temp.model.VCard; +import rocks.xmpp.extensions.version.SoftwareVersionManager; +import rocks.xmpp.extensions.version.model.SoftwareVersion; +import rocks.xmpp.util.XmppUtils; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +/** + * @author ugnich + */ +public class XMPPManager implements NotificationListener { + + private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); + + private ExternalComponent router; + @Inject + private CommandsManager commandsManager; + @Value("${xmppbot_jid:juick@localhost}") + private Jid jid; + @Value("${hostname:localhost}") + private String componentName; + @Value("${component_port:5347}") + private int componentPort; + @Value("${component_host:localhost}") + private String componentHost; + @Value("${xmpp_password:secret}") + private String password; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private ExecutorService executorService; + @Value("${service_user:juick}") + private String serviceUsername; + @Inject + private WebApp webApp; + + private User serviceUser; + + @PostConstruct + public void init() { + logger.info("stream router start connecting to {}", componentPort); + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.Message.class), Extension.of(MessageQuery.class)) + .debugger(LogbackDebugger.class) + .defaultResponseTimeout(Duration.ofMillis(120000)) + .build(); + router = ExternalComponent.create(componentName, password, configuration, componentHost, componentPort); + ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class); + serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); + EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class); + entityCapabilitiesManager.setNode("https://juick.com/caps"); + MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = router.getManager(MessageDeliveryReceiptsManager.class); + messageDeliveryReceiptsManager.setEnabled(true); + PingManager pingManager = router.getManager(PingManager.class); + pingManager.setEnabled(true); + SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); + softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", + System.getProperty("os.name", "generic"))); + VCard vCard = new VCard(); + vCard.setFormattedName("Juick"); + vCard.setBirthday(LocalDate.of(2008, 10, 22)); + try { + vCard.setUrl(new URL("http://juick.com/")); + vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray( + getClass().getClassLoader().getResource("juick.png")))); + } catch (MalformedURLException e) { + logger.error("invalid url", e); + } catch (IOException e) { + logger.warn("invalid resource", e); + } + router.addIQHandler(MessageQuery.class, iq -> { + Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); + warningMessage.setFrom(jid); + warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); + router.send(warningMessage); + return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); + }); + router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { + @Override + protected IQ processRequest(IQ iq) { + if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) + || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { + return iq.createResult(vCard); + } + User user = userService.getUserByName(iq.getTo().getLocal()); + if (!user.isAnonymous()) { + User info = userService.getUserInfo(user); + VCard userVCard = new VCard(); + userVCard.setFormattedName(info.getFullName()); + userVCard.setNickname(user.getName()); + try { + userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); + if (info.getUrl() != null) { + userVCard.setUrl(new URL(info.getUrl())); + } + } catch (MalformedURLException e) { + logger.warn("url exception", e); + } + return iq.createResult(userVCard); + } + return iq.createError(Condition.BAD_REQUEST); + } + }); + router.addInboundMessageListener(e -> { + ClientMessage result = incomingMessage(e.getMessage()); + if (result != null) { + router.send(result); + } + }); + router.addInboundIQListener(e -> { + IQ iq = e.getIQ(); + Jid jid = iq.getTo(); + if (!jid.getDomain().equals(this.jid.getDomain())) { + router.send(iq); + } + }); + FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); + fileTransferManager.addFileTransferOfferListener(e -> { + try { + List allowedTypes = new ArrayList() {{ + add("png"); + add("jpg"); + }}; + String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); + String targetFilename = String.format("%s.%s", + DigestUtils.md5Hex(String.format("%s-%s", + e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); + if (allowedTypes.contains(attachmentExtension)) { + Path filePath = Paths.get(tmpDir, targetFilename); + FileTransfer ft = e.accept(filePath).get(); + ft.addFileTransferStatusListener(st -> { + logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); + if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { + logger.info("transfer completed"); + try { + Jid initiator = e.getInitiator(); + ClientMessage result = incomingMessageJuick( + userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, + jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); + if (result != null) { + router.send(result); + } + } catch (Exception e1) { + logger.error("ft error", e1); + } + + } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { + logger.info("transfer failed", ft.getException()); + Message msg = new Message(); + msg.setType(Message.Type.CHAT); + msg.setFrom(jid); + msg.setTo(e.getInitiator()); + msg.setBody("File transfer failed, please report to us"); + router.sendMessage(msg); + } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { + logger.info("transfer cancelled"); + } + }); + ft.transfer(); + logger.info("transfer started"); + } else { + e.reject(); + logger.info("transfer rejected"); + } + } catch (IOException | InterruptedException | ExecutionException e1) { + logger.error("ft error", e1); + } + }); + router.addConnectionListener(event -> { + if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { + logger.info("component connected"); + } + }); + router.addSessionStatusListener(event -> { + logger.info("event: " + event.getStatus(), event.getThrowable()); + if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { + logger.info("Authenticated, broadcasting..."); + broadcastPresence(null); + } + }); + router.addInboundPresenceListener(event -> { + incomingPresence(event.getPresence()); + }); + executorService.submit(() -> { + try { + router.connect(); + } catch (XmppException e) { + logger.warn("xmpp exception", e); + } + }); + serviceUser = userService.getUserByName(serviceUsername); + } + + private String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { + StringWriter stanzaWriter = new StringWriter(); + XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( + router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); + router.createMarshaller().marshal(stanza, xmppStreamWriter); + xmppStreamWriter.flush(); + xmppStreamWriter.close(); + return stanzaWriter.toString(); + } + + private void sendJuickMessage(com.juick.Message jmsg, List users) { + List jids = new ArrayList<>(); + + for (User user : users) { + jids.addAll(userService.getJIDsbyUID(user.getUid())); + } + com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + for (String jid : jids) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + + public void sendJuickComment(com.juick.Message jmsg, List users) { + String replyQuote; + String replyTo; + + com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) + : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + replyTo = replyMessage.getUser().getName(); + com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); + + String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; + String attachmentUrl = MessageUtils.attachmentUrl(fullReply); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.addExtension(jmsg); + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processMessageEvent(MessageEvent event) { + com.juick.Message msg = event.getMessage(); + List subscribers = event.getUsers(); + if (msg.isService()) { + return; + } + if (MessageUtils.isPM(msg)) { + userService.getJIDsbyUID(msg.getTo().getUid()) + .forEach(userJid -> { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); + if (inroster) { + mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(msg.getText()); + } else { + mm.setFrom(jid); + mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); + } + router.send(ClientMessage.from(mm)); + }); + } else if (MessageUtils.isReply(msg)) { + sendJuickComment(msg, subscribers); + } + else { + sendJuickMessage(msg, subscribers); + } + } + + private ClientMessage makeReply(Jid jidTo, String txt) { + Message reply = new Message(); + reply.setFrom(jid); + reply.setTo(jidTo); + reply.setType(Message.Type.CHAT); + reply.setBody(txt); + return ClientMessage.from(reply); + } + + @Override + public void processSubscribeEvent(SubscribeEvent subscribeEvent) { + + } + + @Override + public void processLikeEvent(LikeEvent likeEvent) { + List users = likeEvent.getSubscribers(); + com.juick.Message jmsg = likeEvent.getMessage(); + User liker = likeEvent.getUser(); + + if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { + userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { + Message xmppMessage = new Message(); + xmppMessage.setFrom(jid); + xmppMessage.setTo(Jid.of(authorJid)); + xmppMessage.setType(Message.Type.CHAT); + xmppMessage.addExtension(jmsg); + xmppMessage.setBody(String.format("%s recommended your post #%d. %s", + liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); + router.send(ClientMessage.from(xmppMessage)); + }); + } + + String txt = "Recommended by @" + liker.getName() + ":\n"; + txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(jmsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid(); + if (jmsg.getReplies() > 0) { + if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { + txt += " (" + jmsg.getReplies() + " reply)"; + } else { + txt += " (" + jmsg.getReplies() + " replies)"; + } + } + txt += " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + userService.getJIDsbyUID(pingEvent.getPinger().getUid()) + .forEach(userJid -> { + Presence p = new Presence(Jid.of(userJid)); + p.setFrom(jid); + p.setPriority((byte) 10); + router.send(ClientPresence.from(p)); + }); + } + + @Override + public void processMessageReadEvent(MessageReadEvent messageReadEvent) { + + } + + @Override + public void processTopEvent(TopEvent topEvent) { + com.juick.Message message = topEvent.getMessage(); + try { + commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); + } catch (Exception e) { + logger.warn("XMPP error", e); + } + } + + private void incomingPresence(Presence p) { + final String username = p.getTo().getLocal(); + final boolean toJuick = username.equals(jid.getLocal()); + + if (p.getType() == null) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().asBareJid()); + reply.setTo(p.getFrom().asBareJid()); + reply.setType(Presence.Type.UNSUBSCRIBE); + router.send(ClientPresence.from(reply)); + } else if (p.getType().equals(Presence.Type.PROBE)) { + int uid_to = 0; + if (!toJuick) { + uid_to = userService.getUIDbyName(username); + } else { + User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); + if (visitor != null) { + userService.updateLastSeen(visitor); + } + } + + if (toJuick || uid_to > 0) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().withResource(jid.getResource())); + reply.setTo(p.getFrom()); + reply.setPriority((byte)10); + if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { + reply.setStatus("Send ON to enable notifications"); + } + router.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + router.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { + boolean canSubscribe = false; + if (toJuick) { + canSubscribe = true; + } else { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + canSubscribe = true; + } + } + if (canSubscribe) { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.SUBSCRIBED); + router.send(ClientPresence.from(reply)); + + reply.setFrom(reply.getFrom().withResource(jid.getResource())); + reply.setPriority((byte) 10); + reply.setType(null); + router.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + router.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { + if (!toJuick) { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + } + } + + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.UNSUBSCRIBED); + router.send(ClientPresence.from(reply)); + } + } + + public ClientMessage incomingMessage(Message msg) { + ClientMessage result = null; + if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { + StanzaError error = msg.getError(); + if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { + // offline query is full, deactivating this jid + if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { + logger.info("{} is inactive now", msg.getFrom()); + return null; + } + } + return null; + } + Jid to = msg.getTo(); + if (to.getDomain().equals(router.getDomain().toEscapedString()) || to.equals(this.jid)) { + User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); + if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { + String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); + return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); + } + URI attachment = URI.create(StringUtils.EMPTY); + OobX oobX = msg.getExtension(OobX.class); + if (oobX != null) { + attachment = oobX.getUri(); + } + try { + return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); + } catch (Exception e1) { + logger.warn("message exception", e1); + } + } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain()) + || to.getDomain().endsWith("." + jid.getDomain()))) { + if (logger.isInfoEnabled()) { + try { + logger.info("unhandled message: {}", stanzaToString(msg)); + } catch (JAXBException | XMLStreamException ex) { + logger.error("JAXB exception", ex); + } + } + } else { + return ClientMessage.from(msg); + } + return result; + } + private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { + if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { + return null; + } + + messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); + + int commandlen = command.length(); + + // COMPATIBILITY + if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { + command = command.substring(3); + } + + if (!jid.getLocal().equals(to)) { + // PM + if (!StringUtils.isEmpty(command)) { + commandsManager.commandPM(user_from, null, to, command); + return null; + } + } + + try { + CommandResult result = commandsManager.processCommand(user_from, command, attachment); + if (StringUtils.isNotBlank(result.getText())) { + return makeReply(from, result.getText()); + } + } catch (Exception e) { + logger.warn("xmpp command exception", e); + return makeReply(from, "Error processing command"); + } + return null; + } + + private void broadcastPresence(Presence.Type type) { + Presence presence = new Presence(); + presence.setFrom(jid); + if (type != null) { + presence.setType(type); + } + userService.getActiveJIDs().forEach(j -> { + try { + presence.setTo(Jid.of(j)); + router.send(ClientPresence.from(presence)); + } catch (IllegalArgumentException ex) { + logger.warn("Invalid jid: {}", j, ex); + } + }); + } + + @PreDestroy + public void close() throws Exception { + broadcastPresence(Presence.Type.UNAVAILABLE); + if (router != null) { + router.close(); + } + } + + public ExternalComponent getRouter() { + return router; + } +} diff --git a/src/main/java/com/juick/server/configuration/XMPPConfig.java b/src/main/java/com/juick/server/configuration/XMPPConfig.java index ffacf4ef..f9b6f092 100644 --- a/src/main/java/com/juick/server/configuration/XMPPConfig.java +++ b/src/main/java/com/juick/server/configuration/XMPPConfig.java @@ -1,21 +1,13 @@ package com.juick.server.configuration; -import com.juick.server.XMPPConnection; +import com.juick.server.XMPPManager; import com.juick.server.xmpp.JidConverter; -import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.server.xmpp.s2s.BasicXmppSession; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import rocks.xmpp.core.session.Extension; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.session.debug.LogbackDebugger; - -import java.time.Duration; @Configuration @ConditionalOnProperty("xmppbot_jid") @@ -23,22 +15,13 @@ public class XMPPConfig { @Value("${hostname:localhost}") private String hostname; @Bean - public BasicXmppSession session() { - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.Message.class), Extension.of(MessageQuery.class)) - .debugger(LogbackDebugger.class) - .defaultResponseTimeout(Duration.ofMillis(120000)) - .build(); - return BasicXmppSession.create(hostname, configuration); - } - @Bean public static ConversionService conversionService() { DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); cs.addConverter(new JidConverter()); return cs; } @Bean - public XMPPConnection xmppConnection() { - return new XMPPConnection(); + public XMPPManager xmppConnection() { + return new XMPPManager(); } } diff --git a/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java b/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java deleted file mode 100644 index ae28f827..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.xmpp.s2s; - -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.XmppSession; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.stanza.model.IQ; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.core.stanza.model.Presence; -import rocks.xmpp.core.stanza.model.server.ServerIQ; -import rocks.xmpp.core.stanza.model.server.ServerMessage; -import rocks.xmpp.core.stanza.model.server.ServerPresence; -import rocks.xmpp.core.stream.model.StreamElement; - -/** - * Created by vitalyster on 06.02.2017. - */ -public class BasicXmppSession extends XmppSession { - protected BasicXmppSession(String xmppServiceDomain, XmppSessionConfiguration configuration) { - super(xmppServiceDomain, configuration); - } - - public static BasicXmppSession create(String xmppServiceDomain, XmppSessionConfiguration configuration) { - BasicXmppSession session = new BasicXmppSession(xmppServiceDomain, configuration); - notifyCreationListeners(session); - return session; - } - - @Override - public void connect(Jid from) throws XmppException { - - } - - @Override - public Jid getConnectedResource() { - return null; - } - - @Override - protected StreamElement prepareElement(StreamElement element) { - if (element instanceof Message) { - element = ServerMessage.from((Message) element); - } else if (element instanceof Presence) { - element = ServerPresence.from((Presence) element); - } else if (element instanceof IQ) { - element = ServerIQ.from((IQ) element); - } - - return element; - } -} diff --git a/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java b/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java deleted file mode 100644 index 6932298f..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.xmpp.s2s; - - -import rocks.xmpp.core.stanza.model.Stanza; - -/** - * Created by vitalyster on 07.12.2016. - */ -public interface StanzaListener { - void stanzaReceived(Stanza xmlValue); -} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 620e210c..fa2e2ce9 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -87,9 +87,6 @@ import rocks.xmpp.addr.Jid; import rocks.xmpp.core.session.Extension; import rocks.xmpp.core.session.XmppSession; import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.stanza.model.StanzaError; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.errors.Condition; import javax.inject.Inject; import javax.servlet.http.Cookie; @@ -101,7 +98,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; -import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -113,8 +109,6 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.*; import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; @@ -161,8 +155,6 @@ public class ServerTests { @Inject private CommandsManager commandsManager; @Inject - private XMPPConnection router; - @Inject private SubscriptionService subscriptionService; @Inject private PrivacyQueriesService privacyQueriesService; -- cgit v1.2.3