/*
* 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.ExternalToken;
import com.juick.User;
import com.juick.formatters.PlainTextFormatter;
import com.turo.pushy.apns.ApnsClient;
import com.turo.pushy.apns.PushNotificationResponse;
import com.turo.pushy.apns.util.ApnsPayloadBuilder;
import com.turo.pushy.apns.util.SimpleApnsPushNotification;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import rocks.xmpp.core.XmppException;
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.concurrent.ExecutionException;
import java.util.concurrent.Future;
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<>());
private final Set invalidAPNSTokens = Collections.synchronizedSet(new HashSet<>());
@Inject
private MPNSClient mpnsClient;
@Inject
private ApnsClient apns;
@Value("${ios_app_id:}")
private String topic;
@PostConstruct
public void init() throws IOException {
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 users = new ArrayList<>();
if (isPM) {
users.addAll(rest.exchange(String.format("http://api.juick.com/notifications?uid=%d",
pmTo),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
} else {
if (isReply) {
users.addAll(rest.exchange(String.format("http://api.juick.com/notifications?uid=%d&mid=%d&rid=%d",
jmsg.getUser().getUid(), jmsg.getMid(), jmsg.getRid()),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
} else {
users.addAll(rest.exchange(String.format("http://api.juick.com/notifications?uid=%s&mid=%s",
jmsg.getUser().getUid(), jmsg.getMid()),
HttpMethod.GET, null, new ParameterizedTypeReference>() {
}).getBody());
}
}
// GCM
List regids = users.stream().flatMap(u -> u.getTokens().stream()).filter(d -> d.getType().equals("gcm"))
.map(ExternalToken::getToken).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 = users.stream().flatMap(u -> u.getTokens().stream()).filter(d -> d.getType().equals("mpns"))
.map(ExternalToken::getToken).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 = users.stream().flatMap(u -> u.getTokens().stream()).filter(d -> d.getType().equals("apns"))
.map(ExternalToken::getToken).collect(Collectors.toList());
if (!tokens.isEmpty()) {
ApnsPayloadBuilder apnsPayloadBuilder = new ApnsPayloadBuilder();
String post = PlainTextFormatter.formatPost(jmsg);
String[] parts = post.split("\n", 2);
String payload = apnsPayloadBuilder.setAlertTitle(parts[0])
.setAlertBody(parts[1]).buildWithDefaultMaximumLength();
for (String token : tokens) {
final Future> notification
= apns.sendNotification(new SimpleApnsPushNotification(token, topic, payload));
try {
final PushNotificationResponse pushNotificationResponse
= notification.get();
if (pushNotificationResponse.isAccepted()) {
logger.info("APNS accepted: {}", token);
} else {
String reason = pushNotificationResponse.getRejectionReason();
logger.info("APNS rejected: {}", reason);
if (reason.equals("BadDeviceToken")) {
invalidAPNSTokens.add(token);
}
}
} catch (final ExecutionException | InterruptedException ex) {
logger.info("APNS exception", ex);
}
}
} else {
logger.info("APNS: no recipients");
}
});
try {
xmpp.connect();
} catch (XmppException e) {
logger.warn("xmpp extension", e);
}
}
@Override
public void close() throws Exception {
apns.close();
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;
}
}
public Set getInvalidAPNSTokens() {
return invalidAPNSTokens;
}
}