/* * 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.components; import com.juick.Attachment; import com.juick.User; import com.juick.components.s2s.BasicXmppSession; import com.juick.server.helpers.UserInfo; import com.juick.service.MessagesService; import com.juick.service.SubscriptionService; import com.juick.service.UserService; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; 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.stanza.AbstractIQHandler; import rocks.xmpp.core.stanza.model.IQ; import rocks.xmpp.core.stanza.model.Message; import rocks.xmpp.core.stanza.model.Stanza; import rocks.xmpp.core.stanza.model.client.ClientMessage; import rocks.xmpp.core.stanza.model.errors.Condition; import rocks.xmpp.extensions.caps.model.EntityCapabilities; 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.vcard.temp.model.VCard; import rocks.xmpp.util.XmppUtils; import javax.annotation.PostConstruct; 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 public class XMPPConnection implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(XMPPConnection.class); private ExternalComponent router; @Inject private JuickBot bot; @Value("${componentname:localhost}") private String componentName; @Value("${component_port:5347}") private int componentPort; @Value("${xmpp_password:secret}") private String password; @Value("${upload_tmp_dir:/tmp}") private String tmpDir; @Inject public MessagesService messagesService; @Inject public UserService userService; @Inject public SubscriptionService subscriptionService; @Inject private BasicXmppSession session; @Inject private ExecutorService service; @PostConstruct public void init() { logger.info("stream router start connecting to {}", componentPort); router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort); PingManager pingManager = router.getManager(PingManager.class); pingManager.setEnabled(true); router.disableFeature(EntityCapabilities.NAMESPACE); 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(VCard.class, new AbstractIQHandler(IQ.Type.GET) { @Override protected IQ processRequest(IQ iq) { if (iq.getTo().equals(bot.getJid()) || iq.getTo().asBareJid().equals(bot.getJid().asBareJid()) || iq.getTo().asBareJid().toEscapedString().equals(bot.getJid().getDomain())) { return iq.createResult(vCard); } User user = userService.getUserByName(iq.getTo().getLocal()); if (user.getUid() > 0) { 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 -> { Message message = e.getMessage(); Jid jid = message.getTo(); if (jid.getDomain().equals(router.getDomain().toEscapedString())) { com.juick.Message jmsg = message.getExtension(com.juick.Message.class); if (jmsg != null) { if (jid.getLocal().equals("recomm")) { sendJuickRecommendation(jmsg); } else { if (jmsg.getRid() > 0) { sendJuickComment(jmsg); } else if (jmsg.getMid() > 0) { sendJuickMessage(jmsg); } } } } else if (jid.getDomain().endsWith(bot.getJid().getDomain()) && (jid.getDomain().equals(bot.getJid().getDomain()) || jid.getDomain().endsWith("." + bot.getJid().getDomain()))) { if (logger.isInfoEnabled()) { try { logger.info("unhandled message: {}", stanzaToString(message)); } catch (JAXBException | XMLStreamException ex) { logger.error("JAXB exception", ex); } } } else { route(ClientMessage.from(message)); } }); router.addInboundIQListener(e -> { IQ iq = e.getIQ(); Jid jid = iq.getTo(); if (!jid.getDomain().equals(bot.getJid().getDomain())) { route(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"); Message msg = new Message(); msg.setType(Message.Type.CHAT); msg.setFrom(e.getInitiator()); msg.setTo(bot.getJid()); msg.setBody(e.getDescription()); try { String attachmentUrl = String.format("juick://%s", targetFilename); msg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); router.sendMessage(msg); } catch (URISyntaxException e1) { logger.warn("attachment 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(bot.getJid()); 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"); } }); service.submit(() -> { try { Thread.sleep(3000); router.connect(); } catch (InterruptedException | XmppException e) { logger.warn("xmpp exception", e); } }); } 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(); } void route(Stanza stanza) { try { String xml = stanzaToString(stanza); logger.info("stream router (out): {}", xml); bot.sendNotification(stanza); } catch (XMLStreamException | JAXBException e) { logger.error("JAXB exception", e); } } public void sendStanza(Stanza xml) { router.send(xml); } public void sendJuickMessage(com.juick.Message jmsg) { List jids = new ArrayList<>(); if (jmsg.FriendsOnly) { jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly); } else { List users = subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()); for (User user : users) { for (String jid : userService.getJIDsbyUID(user.getUid())) { jids.add(jid); } } } com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()); String txt = "@" + jmsg.getUser().getName() + ":" + fullMsg.getTagsString() + "\n"; Attachment attachment = fullMsg.getAttachment(); if (attachment != null) { txt += attachment.getMedium().getUrl() + "\n"; } txt += jmsg.getText() + "\n\n"; txt += "#" + jmsg.getMid() + " http://juick.com/" + jmsg.getMid(); Nickname nick = new Nickname("@" + jmsg.getUser().getName()); Message msg = new Message(); msg.setFrom(bot.getJid()); msg.setBody(txt); msg.setType(Message.Type.CHAT); msg.setThread("juick-" + jmsg.getMid()); msg.addExtension(jmsg); msg.addExtension(nick); if (attachment != null) { try { OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); msg.addExtension(oob); } catch (URISyntaxException e) { logger.warn("uri exception", e); } } for (String jid : jids) { msg.setTo(Jid.of(jid)); route(ClientMessage.from(msg)); } } public void sendJuickComment(com.juick.Message jmsg) { List users; String replyQuote; String replyTo; users = subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()); 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 = fullReply.getReplyQuote(); String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; Attachment attachment = jmsg.getAttachment(); if (attachment != null) { txt += attachment.getMedium().getUrl() + "\n"; } txt += jmsg.getText() + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/" + jmsg.getMid() + "#" + jmsg.getRid(); Message msg = new Message(); msg.setFrom(bot.getJid()); 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)); route(ClientMessage.from(msg)); } } } public void sendJuickRecommendation(com.juick.Message recomm) { List users; com.juick.Message jmsg = messagesService.getMessage(recomm.getMid()); users = subscriptionService.getUsersSubscribedToUserRecommendations(recomm.getUser().getUid(), recomm.getMid(), jmsg.getUser().getUid()); String txt = "Recommended by @" + recomm.getUser().getName() + ":\n"; txt += "@" + jmsg.getUser().getName() + ":" + jmsg.getTagsString() + "\n"; Attachment attachment = jmsg.getAttachment(); if (attachment != null) { txt += attachment.getMedium().getUrl() + "\n"; } txt += 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/" + jmsg.getMid(); Nickname nick = new Nickname("@" + jmsg.getUser().getName()); Message msg = new Message(); msg.setFrom(bot.getJid()); msg.setBody(txt); msg.setType(Message.Type.CHAT); msg.setThread("juick-" + jmsg.getMid()); msg.addExtension(jmsg); msg.addExtension(nick); if (attachment != null) { try { OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); 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)); route(ClientMessage.from(msg)); } } } @Override public void close() throws Exception { if (router != null) { router.close(); } } }