/* * 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.push; import com.google.android.gcm.server.Message; import com.google.android.gcm.server.MulticastResult; import com.google.android.gcm.server.Result; import com.google.android.gcm.server.Sender; import com.juick.json.MessageSerializer; import com.juick.server.PushQueries; import com.juick.xmpp.JID; import com.juick.xmpp.Message.MessageListener; import com.juick.xmpp.utils.XmlUtils; import com.juick.xmpp.Stream; import com.juick.xmpp.StreamComponent; import com.juick.xmpp.extensions.JuickMessage; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsService; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.Socket; import java.net.URL; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Ugnich Anton */ public class PushComponent implements ServletContextListener, Stream.StreamListener, MessageListener { private static Logger logger = Logger.getLogger(PushComponent.class.getName()); Connection sql; Socket socket; Stream xmpp; Sender GCMSender; @Override public void contextInitialized(ServletContextEvent sce) { logger.info("component initialized"); Properties conf = new Properties(); try { conf.load(sce.getServletContext().getResourceAsStream("WEB-INF/push.conf")); GCMSender = new Sender(conf.getProperty("gcm_key")); setupSql(conf.getProperty("mysql_host"), conf.getProperty("mysql_username"), conf.getProperty("mysql_password", ""), conf.getProperty("mysql_database", "")); setupXmppComponent(new JID("", conf.getProperty("xmpp_jid"), ""), conf.getProperty("xmpp_host", "localhost"), Integer.parseInt(conf.getProperty("xmpp_port", "5347")), conf.getProperty("xmpp_password", "")); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); } } @Override public void contextDestroyed(ServletContextEvent sce) { logger.info("component destroyed"); } public void setupSql(String host, String username, String password, String database) { try { sql = DriverManager.getConnection( String.format("jdbc:mysql://%s/%s?autoReconnect=true&user=%s&password=%s", host, database, username, password)); } catch (SQLException e) { logger.log(Level.SEVERE, e.getMessage(), e); } } public void setupXmppComponent(JID jid, String host, int port, String password) { try { socket = new Socket(host, port); xmpp = new StreamComponent(jid, socket.getInputStream(), socket.getOutputStream(), password); xmpp.addChildParser(new JuickMessage()); xmpp.addListener((Stream.StreamListener) this); xmpp.addListener((MessageListener) this); xmpp.startParsing(); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); } } @Override public void onStreamReady() { logger.info("XMPP STREAM READY"); } @Override public void onStreamFail(String msg) { logger.warning("XMPP STREAM FAIL: " + msg); } @Override public void onMessage(com.juick.xmpp.Message msg) { JuickMessage jmsg = (JuickMessage) msg.getChild(JuickMessage.XMLNS); if (jmsg == null) { return; } int uid_to = 0; try { uid_to = Integer.parseInt(msg.to.Username); } catch (Exception e) { logger.log(Level.SEVERE, e.getMessage(), e); } /*** ANDROID ***/ ArrayList regids; if (uid_to > 0) { regids = new ArrayList(); String targetId = PushQueries.getAndroidRegID(sql, uid_to); if (targetId != null && !targetId.isEmpty()) { regids.add(targetId); } } else { regids = PushQueries.getAndroidSubscribers(sql, jmsg.User.UID); } if (!regids.isEmpty()) { MessageSerializer messageSerializer = new MessageSerializer(); String json = messageSerializer.serialize(jmsg).toString(); System.out.println(json); Message message = new Message.Builder().addData("message", json).build(); try { MulticastResult result = GCMSender.send(message, regids, 3); List results = result.getResults(); for (int i = 0; i < results.size(); i++) { logger.fine("RES " + i + ": " + results.get(i).toString()); } } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); } catch (IllegalArgumentException err) { logger.warning("Android: Invalid API Key"); } } /*** WinPhone ***/ ArrayList urls; if (uid_to > 0) { urls = new ArrayList(); String targetURL = PushQueries.getWinPhoneURL(sql, uid_to); if (targetURL != null && !targetURL.isEmpty()) { urls.add(targetURL); } } else { urls = PushQueries.getWinPhoneSubscribers(sql, jmsg.User.UID); } if (!urls.isEmpty()) { String text1 = "@" + jmsg.User.UName; if (!jmsg.Tags.isEmpty()) { text1 += ":" + XmlUtils.escape(jmsg.getTagsString()); } String text2; if (jmsg.Text.length() > 250) { text2 = XmlUtils.escape(jmsg.Text.substring(0, 255)) + "..."; } else { text2 = XmlUtils.escape(jmsg.Text); } String xml = "" + "" + "" + "" + text1 + "" + "" + text2 + "" + "?mid=" + jmsg.MID + "" + "" + ""; logger.fine(xml); for (int i = 0; i < urls.size(); i++) { String url = urls.get(i); logger.fine("MPNS: " + url); sendMPNS(url, xml); } } /*** iOS ***/ List tokens; if (uid_to > 0) { tokens = new ArrayList(); String targetToken = PushQueries.getAPNSToken(sql, uid_to); if (targetToken != null && !targetToken.isEmpty()) { tokens.add(targetToken); } } else { tokens = PushQueries.getAPNSSubscribers(sql, jmsg.User.UID); } if (!tokens.isEmpty()) { ApnsService service = APNS.newService().withCert("/etc/juick/ios.p12", "juick") .withSandboxDestination().build(); for (String token : tokens) { String payload = APNS.newPayload().alertTitle("@" + jmsg.User.UName).alertBody(jmsg.Text).build(); logger.fine("APNS: " + token); service.push(token, payload); } } } public boolean sendMPNS(String url, String xml) { boolean ret = false; try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8"); conn.setRequestProperty("X-WindowsPhone-Target", "toast"); conn.setRequestProperty("X-NotificationClass", "2"); conn.setUseCaches(false); conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.connect(); OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); wr.write(xml); wr.close(); if (conn.getResponseCode() == 200) { ret = streamToString(conn.getInputStream()) != null; } conn.disconnect(); } catch (Exception e) { logger.severe("sendMPNS: " + e.toString()); } return ret; } public String streamToString(InputStream is) { try { BufferedReader buf = new BufferedReader(new InputStreamReader(is)); StringBuilder str = new StringBuilder(); String line; do { line = buf.readLine(); str.append(line).append("\n"); } while (line != null); return str.toString(); } catch (Exception e) { logger.severe("streamToString: " + e.toString()); } return null; } }