/* * 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.components.mpns.MPNSError; import com.juick.components.mpns.MPNSToken; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsService; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.http.Consts; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.apache.http.util.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.env.Environment; 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.ArrayList; import java.util.Arrays; import java.util.List; /** * @author Ugnich Anton */ public class Notifications implements AutoCloseable { private static Logger logger = LoggerFactory.getLogger(Notifications.class); private final RestTemplate rest; private ExternalComponent xmpp; private final Sender GCMSender; private final String wns_application_sip; private final String wns_client_secret; private final String pushJid; private final String xmppHost; private final int xmppPort; private final String xmppPushPassword; private final ObjectMapper mapper; @Inject private ApnsService apns; public Notifications(final Environment env, final RestTemplate rest) { this.rest = rest; wns_application_sip = env.getProperty("wns_application_sip", ""); wns_client_secret = env.getProperty("wns_client_secret", ""); GCMSender = new Sender(env.getProperty("gcm_key", ""), Endpoint.GCM); pushJid = env.getProperty("push_jid"); xmppHost = env.getProperty("xmpp_host", "localhost"); xmppPort = NumberUtils.toInt(env.getProperty("xmpp_port"), 5347); xmppPushPassword = env.getProperty("push_xmpp_password", ""); mapper = new ObjectMapper(); } @PostConstruct public void init() { XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() .extensions(Extension.of(com.juick.Message.class)) .build(); xmpp = ExternalComponent.create(pushJid, xmppPushPassword, configuration, xmppHost, xmppPort); 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); /*** ANDROID ***/ final List regids = new ArrayList<>(); if (isPM) { regids.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=gcm&uid=%d", pmTo), HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody()); } else { regids.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()); } if (!regids.isEmpty()) { try { ObjectMapper messageSerializer = new ObjectMapper(); String json = messageSerializer.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++) { logger.info("RES " + i + ": " + results.get(i).toString()); } } catch (IOException ex) { logger.error(ex.getMessage(), ex); } catch (IllegalArgumentException err) { logger.warn("Android: Invalid API Key"); } } else { logger.info("GMS: no recipients"); } /*** WinPhone ***/ final List urls = new ArrayList<>(); if (isPM) { urls.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=mpns&uid=%s", pmTo), HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody()); } else { urls.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=mpns&uid=%s&mid=%s", jmsg.getUser().getUid(), jmsg.getMid()), HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody()); } if (urls.isEmpty()) { logger.info("WNS: no recipients"); } else { try { String wnsToken = getWnsAccessToken(); 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); sendWNS(wnsToken, url, xml); } } catch (IOException | IllegalStateException ex) { logger.error("WNS: ", ex); } } /*** iOS ***/ final List tokens = new ArrayList<>(); if (isPM) { tokens.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=apns&uid=%s", pmTo), HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody()); } else { tokens.addAll(rest.exchange(String.format("http://api.juick.com/notifications?type=apns&uid=%s&mid=%s", jmsg.getUser().getUid(), jmsg.getMid()), HttpMethod.GET, null, new ParameterizedTypeReference>() { }).getBody()); } 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"); } String getWnsAccessToken() throws IOException, IllegalStateException { if (TextUtils.isEmpty(wns_application_sip)) { throw new IllegalStateException("'wns_application_sip' is not initialized"); } if (TextUtils.isEmpty(wns_client_secret)) { throw new IllegalStateException("'wns_client_secret' is not initialized"); } HttpClient client = HttpClientBuilder.create().build(); String url = "https://login.live.com/accesstoken.srf"; List formParams = new ArrayList<>(); formParams.add(new BasicNameValuePair("grant_type", "client_credentials")); formParams.add(new BasicNameValuePair("client_id", wns_application_sip)); formParams.add(new BasicNameValuePair("client_secret", wns_client_secret)); formParams.add(new BasicNameValuePair("scope", "notify.windows.com")); UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); HttpPost httppost = new HttpPost(url); httppost.setEntity(entity); HttpResponse response = client.execute(httppost); int statusCode = response.getStatusLine().getStatusCode(); String responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8); if (statusCode != 200) { MPNSError error = mapper.readValue(responseContent, MPNSError.class); throw new IOException(error.getError() + ": " + error.getErrorDescription()); } MPNSToken token = mapper.readValue(responseContent, MPNSToken.class); if (token.getTokenType().length() >= 1) { token.setTokenType(Character.toUpperCase(token.getTokenType().charAt(0)) + token.getTokenType().substring(1)); } return token.getTokenType() + " " + token.getAccessToken(); } void sendWNS(final String wnsToken, final String url, final String xml) throws IOException { HttpClient client = HttpClientBuilder.create().build(); StringEntity entity = new StringEntity(xml, Consts.UTF_8); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Content-Type", "text/xml"); httpPost.setHeader("Authorization", wnsToken); httpPost.setHeader("X-WNS-Type", "wns/toast"); httpPost.setEntity(entity); HttpResponse response = client.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { String headersContent = stringifyWnsHttpHeaders(response.getAllHeaders()); throw new IOException(headersContent); } } static String stringifyWnsHttpHeaders(final Header[] allHeaders) { String[] wnsHeaders = Arrays.stream(allHeaders) .filter(x -> x.getName().startsWith("X-WNS-") || x.getName().startsWith("WWW-")) .map(x -> x.getName() + ": " + x.getValue()) .toArray(String[]::new); return String.join("\n", wnsHeaders); } }