/* * 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.server.component.*; import com.juick.server.helpers.CommandResult; import com.juick.server.helpers.UserInfo; import com.juick.server.xmpp.iq.MessageQuery; import com.juick.server.xmpp.s2s.BasicXmppSession; import com.juick.server.xmpp.s2s.StanzaListener; 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 org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; 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.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.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; /** * @author ugnich */ @Component @DependsOn("XMPPRouter") public class XMPPConnection implements StanzaListener, NotificationListener { private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); private ExternalComponent router; @Inject private XMPPServer xmpp; @Inject private CommandsManager commandsManager; @Value("${xmppbot_jid:juick@localhost}") private Jid jid; @Value("${componentname:localhost}") private String componentName; @Value("${component_port:5347}") private int componentPort; @Value("${xmpp_password:secret}") private String password; @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String tmpDir; @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String imgDir; @Inject private MessagesService messagesService; @Inject private UserService userService; @Inject private SubscriptionService subscriptionService; @Inject private PMQueriesService pmQueriesService; @Inject private TagService tagService; @Inject private BasicXmppSession session; @Inject private ExecutorService service; @Inject private ApplicationEventPublisher applicationEventPublisher; @PostConstruct public void init() { logger.info("stream router start connecting to {}", componentPort); xmpp.addStanzaListener(this); router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort); 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"); 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()) { UserInfo info = userService.getUserInfo(user); VCard userVCard = new VCard(); userVCard.setFormattedName(info.getFullName()); userVCard.setNickname(user.getName()); try { userVCard.setPhoto(new VCard.Image(new URI("http://i.juick.com/a/" + user.getUid() + ".png"))); if (info.getUrl() != null) { userVCard.setUrl(new URL(info.getUrl())); } } catch (MalformedURLException | URISyntaxException 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, e.getDescription(), 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()); }); service.submit(() -> { try { router.connect(); } catch (XmppException e) { logger.warn("xmpp exception", e); } }); } 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<>(); if (jmsg.FriendsOnly) { jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly); } else { for (User user : users) { jids.addAll(userService.getJIDsbyUID(user.getUid())); } } com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()); 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()); 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(); 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) { } 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); } 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."); } com.juick.Message jmsg = msg.getExtension(com.juick.Message.class); if (jmsg != null) { if (MessageUtils.isReply(jmsg)) { // to get quote and attachment com.juick.Message original = messagesService.getMessage(jmsg.getMid()); com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); applicationEventPublisher.publishEvent(new MessageEvent(this, reply, subscriptionService.getUsersSubscribedToComments(original, reply))); } else if (!MessageUtils.isPM(jmsg)) { applicationEventPublisher.publishEvent(new MessageEvent(this, messagesService.getMessage(jmsg.getMid()), subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()))); } } else { URI attachment = URI.create(StringUtils.EMPTY); OobX oobX = msg.getExtension(OobX.class); if (oobX != null) { attachment = oobX.getUri(); } try { if (msg.getTo().asBareJid().equals(jid.asBareJid())) { return incomingMessageJuick(user_from, msg.getFrom(), StringUtils.defaultString(msg.getBody()), attachment); } else { // PM result = incomingMessageJuick(user_from, msg.getFrom(), String.format("@%s %s", msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody())), 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 command, @Nonnull URI attachment) { if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { return null; } int commandlen = command.length(); // COMPATIBILITY if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { command = command.substring(3); } try { CommandResult result = commandsManager.processCommand(user_from, command.trim(), 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; } @Override public void stanzaReceived(Stanza xmlValue) { router.send(xmlValue); } 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; } }