/*
* Juick
* Copyright (C) 2013, Ugnich Anton
*
* 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.fasterxml.jackson.databind.ObjectMapper;
import com.google.android.gcm.server.*;
import com.juick.TokensList;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import rocks.xmpp.core.XmppException;
import rocks.xmpp.core.session.Extension;
import rocks.xmpp.core.session.XmppSessionConfiguration;
import rocks.xmpp.extensions.component.accept.ExternalComponent;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Ugnich Anton
*/
public class Notifications implements NotificationClientListener, AutoCloseable {
private static Logger logger = LoggerFactory.getLogger(Notifications.class);
@Inject
private RestTemplate rest;
@Inject
private ExternalComponent xmpp;
@Inject
private Sender GCMSender;
@Inject
private ObjectMapper jsonMapper;
private final Set invalidGCMTokens = Collections.synchronizedSet(new HashSet<>());
private final Set invalidMPNSTokens = Collections.synchronizedSet(new HashSet<>());
@Inject
private MPNSClient mpnsClient;
@Inject
private ApnsService apns;
@PostConstruct
public void init() throws IOException {
mpnsClient.authenticate();
mpnsClient.setListener(this);
xmpp.addInboundMessageListener(e -> {
rocks.xmpp.core.stanza.model.Message msg = e.getMessage();
com.juick.Message jmsg = msg.getExtension(com.juick.Message.class);
boolean isPM = jmsg.getMid() == 0;
boolean isReply = jmsg.getRid() > 0;
int pmTo = NumberUtils.toInt(msg.getTo().getLocal(), 0);
final List tokensList = new ArrayList<>();
if (isPM) {
tokensList.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=gcm&uid=%d",
pmTo),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
} else {
if (isReply) {
tokensList.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=gcm&uid=%d&mid=%d&rid=%d",
jmsg.getUser().getUid(), jmsg.getMid(), jmsg.getRid()),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
} else {
tokensList.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=gcm&uid=%s&mid=%s",
jmsg.getUser().getUid(), jmsg.getMid()),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
}
}
// GCM
List regids = tokensList.stream().filter(t -> t.getType().equals("gcm"))
.flatMap(t -> t.getTokens().stream()).collect(Collectors.toList());
if (!regids.isEmpty()) {
try {
String json = jsonMapper.writeValueAsString(jmsg);
logger.info(json);
Message message = new Message.Builder().addData("message", json).build();
MulticastResult result = GCMSender.send(message, regids, 3);
List results = result.getResults();
for (int i = 0; i < results.size(); i++) {
Result currentResult = results.get(i);
logger.info("RES {}: {}", i, currentResult);
List errorCodes = Arrays.asList(Constants.ERROR_MISMATCH_SENDER_ID, Constants.ERROR_NOT_REGISTERED);
if (errorCodes.contains(currentResult.getErrorCodeName())) {
// assuming results are in order of regids
// http://stackoverflow.com/a/11594531/1097384
String currentId = regids.get(i);
logger.info("{} is scheduled to remove", currentId);
addInvalidGCMToken(currentId);
}
}
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
} catch (IllegalArgumentException err) {
logger.warn("Android: Invalid API Key", err);
}
} else {
logger.info("GMS: no recipients");
}
/*** WinPhone ***/
List urls = tokensList.stream().filter(t -> t.getType().equals("mpns"))
.flatMap(t -> t.getTokens().stream()).collect(Collectors.toList());
if (urls.isEmpty()) {
logger.info("WNS: no recipients");
} else {
try {
String text1 = "@" + jmsg.getUser().getName();
if (!jmsg.getTags().isEmpty()) {
text1 += ":" + StringEscapeUtils.escapeXml11(jmsg.getTagsString());
}
String text2 = StringEscapeUtils.escapeXml11(jmsg.getText());
String xml = ""
+ ""
+ ""
+ ""
+ ""
+ "" + text1 + ""
+ "" + text2 + ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
logger.trace(xml);
for (String url : urls) {
logger.info("WNS: {}", url);
mpnsClient.sendNotification(url, xml);
}
} catch (IOException | IllegalStateException ex) {
logger.error("WNS: ", ex);
}
}
/*** iOS ***/
List tokens = tokensList.stream().filter(t -> t.getType().equals("apns"))
.flatMap(t -> t.getTokens().stream()).collect(Collectors.toList());
if (!tokens.isEmpty()) {
for (String token : tokens) {
String payload = APNS.newPayload().alertTitle("@" + jmsg.getUser().getName()).alertBody(jmsg.getText()).build();
logger.info("APNS: {}", token);
apns.push(token, payload);
}
} else {
logger.info("APNS: no recipients");
}
});
try {
xmpp.connect();
} catch (XmppException e) {
logger.warn("xmpp extension", e);
}
}
@Override
public void close() throws Exception {
if (xmpp != null)
xmpp.close();
logger.info("ExternalComponent on notifications destroyed");
}
public void addInvalidGCMToken(String token) {
synchronized (invalidGCMTokens) {
invalidGCMTokens.add(token);
}
}
public Set getInvalidGCMTokens() {
return invalidGCMTokens;
}
public void cleanupGCMTokens() {
logger.info("removed {} GCM tokens", invalidGCMTokens.size());
synchronized (invalidGCMTokens) {
invalidGCMTokens.clear();
}
}
public void addInvalidMPNSToken(String token) {
synchronized (invalidMPNSTokens) {
invalidMPNSTokens.add(token);
}
}
public Set getInvalidMPNSTokens() {
return invalidMPNSTokens;
}
public void cleanupMPNSTokens() {
logger.info("removed {} MPNS tokens", invalidMPNSTokens.size());
synchronized (invalidMPNSTokens) {
invalidMPNSTokens.clear();
}
}
@Override
public void invalidToken(String type, String token) {
switch (type) {
case "mpns":
addInvalidMPNSToken(token);
break;
default:
break;
}
}
}