diff options
Diffstat (limited to 'juick-server/src/main/java/com/juick/server/XMPPConnection.java')
-rw-r--r-- | juick-server/src/main/java/com/juick/server/XMPPConnection.java | 412 |
1 files changed, 412 insertions, 0 deletions
diff --git a/juick-server/src/main/java/com/juick/server/XMPPConnection.java b/juick-server/src/main/java/com/juick/server/XMPPConnection.java new file mode 100644 index 00000000..f5448880 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/XMPPConnection.java @@ -0,0 +1,412 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.juick.server; + +import com.juick.Attachment; +import com.juick.User; +import com.juick.xmpp.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.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +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.extensions.version.SoftwareVersionManager; +import rocks.xmpp.extensions.version.model.SoftwareVersion; +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 XMPPBot 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); + 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(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<String> allowedTypes = new ArrayList<String>() {{ + 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<String> jids = new ArrayList<>(); + + if (jmsg.FriendsOnly) { + jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly); + } else { + List<User> 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 += StringUtils.defaultString(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<User> 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 += StringUtils.defaultString(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<User> 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 += 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/" + 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(); + } + } +} |