/*
* Copyright (C) 2008-2019, 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.model.User;
import com.juick.formatters.PlainTextFormatter;
import com.juick.model.CommandResult;
import com.juick.www.api.SystemActivity;
import com.juick.www.WebApp;
import com.juick.server.xmpp.iq.MessageQuery;
import com.juick.service.MessagesService;
import com.juick.service.PMQueriesService;
import com.juick.service.UserService;
import com.juick.service.component.*;
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.core.io.Resource;
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.IQ;
import rocks.xmpp.core.stanza.model.Message;
import rocks.xmpp.core.stanza.model.Presence;
import rocks.xmpp.core.stanza.model.StanzaError;
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 javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import java.io.IOException;
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.Executor;
/**
* @author ugnich
*/
public class XMPPManager implements NotificationListener {
private static final Logger logger = LoggerFactory.getLogger("XMPP");
private ExternalComponent xmpp;
@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;
@Value("classpath:juick.png")
private Resource vCardImage;
@Inject
private MessagesService messagesService;
@Inject
private UserService userService;
@Inject
private PMQueriesService pmQueriesService;
@Inject
private Executor applicationTaskExecutor;
@Value("${service_user:juick}")
private String serviceUsername;
@Inject
private WebApp webApp;
private User serviceUser;
@PostConstruct
public void init() {
logger.info("xmpp component start connecting to {}", componentPort);
XmppSessionConfiguration configuration = XmppSessionConfiguration.builder()
.extensions(Extension.of(com.juick.model.Message.class), Extension.of(MessageQuery.class))
.debugger(LogbackDebugger.class)
.defaultResponseTimeout(Duration.ofMillis(120000))
.build();
xmpp = ExternalComponent.create(componentName, password, configuration, componentHost, componentPort);
ServiceDiscoveryManager serviceDiscoveryManager = xmpp.getManager(ServiceDiscoveryManager.class);
serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick"));
EntityCapabilitiesManager entityCapabilitiesManager = xmpp.getManager(EntityCapabilitiesManager.class);
entityCapabilitiesManager.setNode("https://juick.com/caps");
MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = xmpp.getManager(MessageDeliveryReceiptsManager.class);
messageDeliveryReceiptsManager.setEnabled(true);
PingManager pingManager = xmpp.getManager(PingManager.class);
pingManager.setEnabled(true);
SoftwareVersionManager softwareVersionManager = xmpp.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(vCardImage.getInputStream())));
} catch (MalformedURLException e) {
logger.error("invalid url", e);
} catch (IOException e) {
logger.warn("invalid resource", e);
}
xmpp.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");
xmpp.send(warningMessage);
return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam"));
});
xmpp.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);
}
});
xmpp.addInboundMessageListener(e -> {
ClientMessage result = incomingMessage(e.getMessage());
if (result != null) {
xmpp.send(result);
}
});
FileTransferManager fileTransferManager = xmpp.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) {
xmpp.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");
xmpp.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);
}
});
xmpp.addConnectionListener(event -> {
if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) {
logger.info("component connected");
}
});
xmpp.addSessionStatusListener(event -> {
logger.info("event: " + event.getStatus(), event.getThrowable());
if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) {
logger.info("Authenticated, broadcasting...");
broadcastPresence(null);
}
});
xmpp.addInboundPresenceListener(event -> {
incomingPresence(event.getPresence());
});
applicationTaskExecutor.execute(() -> {
try {
xmpp.connect();
} catch (XmppException e) {
logger.warn("xmpp exception", e);
}
});
serviceUser = userService.getUserByName(serviceUsername);
}
private void sendJuickMessage(com.juick.model.Message jmsg, List users) {
List jids = new ArrayList<>();
for (User user : users) {
jids.addAll(userService.getJIDsbyUID(user.getUid()));
}
com.juick.model.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));
xmpp.send(ClientMessage.from(msg));
}
}
private void sendJuickComment(com.juick.model.Message jmsg, List users) {
String replyQuote;
String replyTo;
com.juick.model.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto())
: messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new);
replyTo = replyMessage.getUser().getName();
com.juick.model.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));
xmpp.send(ClientMessage.from(msg));
}
}
}
@Override
public void processSystemEvent(SystemEvent systemEvent) {
var activity = systemEvent.getActivity();
var type = activity.getType();
if (type.equals(SystemActivity.ActivityType.message)) {
processMessage(activity.getMessage(), activity.getTo());
} else if (type.equals(SystemActivity.ActivityType.like)) {
if (systemEvent.getActivity().getFrom().equals(serviceUser)) {
processTop(systemEvent.getActivity().getMessage());
} else {
processLike(activity.getFrom(), activity.getMessage(), activity.getTo());
}
}
}
private void processMessage(com.juick.model.Message msg, List subscribers) {
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());
}
xmpp.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);
}
public void processLike(User liker, com.juick.model.Message jmsg, List users) {
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)));
xmpp.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));
xmpp.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);
xmpp.send(ClientPresence.from(p));
});
}
public void processTop(com.juick.model.Message message) {
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);
xmpp.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");
}
xmpp.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));
xmpp.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);
xmpp.send(ClientPresence.from(reply));
reply.setFrom(reply.getFrom().withResource(jid.getResource()));
reply.setPriority((byte) 10);
reply.setType(null);
xmpp.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));
xmpp.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);
xmpp.send(ClientPresence.from(reply));
}
}
public ClientMessage incomingMessage(Message msg) {
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(xmpp.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);
}
}
ClientMessage errorMessage = ClientMessage.from(msg);
errorMessage.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND));
return errorMessage;
}
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));
xmpp.send(ClientPresence.from(presence));
} catch (IllegalArgumentException ex) {
logger.warn("Invalid jid: {}", j, ex);
}
});
}
@PreDestroy
public void close() throws Exception {
broadcastPresence(Presence.Type.UNAVAILABLE);
if (xmpp != null) {
xmpp.close();
}
}
}