/*
* 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.LikeEvent;
import com.juick.server.component.MessageEvent;
import com.juick.server.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 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.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 XMPPServer xmpp;
@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("${xmpp_disabled:false}")
private boolean isXmppDisabled;
@Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
private String tmpDir;
@Inject
private MessagesService messagesService;
@Inject
private UserService userService;
@Inject
private SubscriptionService subscriptionService;
@Inject
private BasicXmppSession session;
@Inject
private ExecutorService service;
@Inject
private ApplicationEventPublisher applicationEventPublisher;
@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(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.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();
logger.info("XMPP is alive and receive message");
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);
User fromUser = jmsg.getUser();
com.juick.Message msg = messagesService.getMessage(jmsg.getMid());
applicationEventPublisher.publishEvent(new LikeEvent(this, fromUser, msg));
} else {
if (jmsg.getRid() > 0) {
// to get quote and attachment
com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid());
sendJuickComment(reply);
applicationEventPublisher.publishEvent(new MessageEvent(this, reply));
} else if (jmsg.getMid() > 0) {
sendJuickMessage(jmsg);
applicationEventPublisher.publishEvent(new MessageEvent(this,
messagesService.getMessage(jmsg.getMid())));
}
}
}
} else if (jid.getDomain().endsWith(jid.getDomain()) && (jid.getDomain().equals(this.jid.getDomain())
|| jid.getDomain().endsWith("." + jid.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(this.jid.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(jid);
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(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");
}
});
if (!isXmppDisabled) {
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.debug("stream router (out): {}", xml);
xmpp.sendOut(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) {
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/" + 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));
route(ClientMessage.from(msg));
}
}
public void sendJuickComment(com.juick.Message jmsg) {
List users;
String replyQuote;
String replyTo;
com.juick.Message op = messagesService.getMessage(jmsg.getMid());
users = subscriptionService.getUsersSubscribedToComments(op, jmsg);
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 + " ";
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/" + 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));
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() + ":" + 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/" + 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));
route(ClientMessage.from(msg));
}
}
}
@Override
public void close() throws Exception {
if (router != null) {
router.close();
}
}
public ExternalComponent getRouter() {
return router;
}
}