diff options
Diffstat (limited to 'juick-www/src/main/java/com/juick/www/PushComponent.java')
-rw-r--r-- | juick-www/src/main/java/com/juick/www/PushComponent.java | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/juick-www/src/main/java/com/juick/www/PushComponent.java b/juick-www/src/main/java/com/juick/www/PushComponent.java new file mode 100644 index 00000000..67366628 --- /dev/null +++ b/juick-www/src/main/java/com/juick/www/PushComponent.java @@ -0,0 +1,311 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + */ +package com.juick.www; + +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.server.SubscriptionsQueries; +import com.juick.xmpp.JID; +import com.juick.xmpp.Message.MessageListener; +import com.juick.xmpp.Stream; +import com.juick.xmpp.StreamComponent; +import com.juick.xmpp.extensions.JuickMessage; +import com.juick.xmpp.utils.XmlUtils; +import com.notnoop.apns.APNS; +import com.notnoop.apns.ApnsService; +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.json.JSONObject; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * + * @author Ugnich Anton + */ +public class PushComponent implements ServletContextListener, Stream.StreamListener, MessageListener { + + private static Logger logger = Logger.getLogger(PushComponent.class.getName()); + + private ExecutorService executorService; + String wns_application_sip; + String wns_client_secret; + JdbcTemplate sql; + Socket socket; + Stream xmpp; + Sender GCMSender; + + @Override + public void contextInitialized(final ServletContextEvent sce) { + logger.info("component initialized"); + executorService = Executors.newSingleThreadExecutor(); + executorService.submit((Runnable) () -> { + Properties conf = new Properties(); + try { + conf.load(sce.getServletContext().getResourceAsStream("/WEB-INF/juick.conf")); + LogManager.getLogManager().readConfiguration( + sce.getServletContext().getResourceAsStream("/WEB-INF/logging.properties")); + wns_application_sip = conf.getProperty("wns_application_sip", ""); + wns_client_secret = conf.getProperty("wns_client_secret", ""); + GCMSender = new Sender(conf.getProperty("gcm_key")); + + setupSql(conf.getProperty("datasource_driver", "com.mysql.jdbc.Driver"), conf.getProperty("datasource_url", "")); + setupXmppComponent(new JID("", conf.getProperty("push_jid"), ""), conf.getProperty("xmpp_host", "localhost"), + Integer.parseInt(conf.getProperty("xmpp_port", "5347")), conf.getProperty("push_xmpp_password", "")); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + }); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + executorService.shutdown(); + logger.info("component destroyed"); + } + + public void setupSql(String driver, String url) { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName(driver); + dataSource.setUrl(url); + sql = new JdbcTemplate(dataSource); + } + + 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(Exception e) {logger.log(Level.SEVERE, "XMPP STREAM FAIL", e);} + + @Override + public void onMessage(com.juick.xmpp.Message msg) { + JuickMessage jmsg = (JuickMessage)msg.getChild(JuickMessage.XMLNS); + List<com.juick.User> subscribedUsers = new ArrayList<>(); + boolean isPM = jmsg.getMID() == 0; + boolean isReply = jmsg.getRID() > 0; + int pmTo = 0; + if (isPM) { + pmTo = Integer.parseInt(msg.to.Username); + } else { + if (isReply) { + subscribedUsers = + SubscriptionsQueries.getUsersSubscribedToComments(sql, jmsg.getMID(), jmsg.getUser().getUID()); + } else { + // new message + subscribedUsers = SubscriptionsQueries.getSubscribedUsers(sql, jmsg.getUser().getUID(), jmsg.getMID()); + } + } + + /*** ANDROID ***/ + final List<String> regids = new ArrayList<>(); + if (isPM) { + PushQueries.getAndroidRegID(sql, pmTo).ifPresent(regids::add); + } else { + List<Integer> uids = subscribedUsers.stream().map(com.juick.User::getUID).collect(Collectors.toList()); + if (uids.size() > 0) { + regids.addAll(PushQueries.getAndroidTokens(sql, uids)); + } + } + + if (!regids.isEmpty()) { + MessageSerializer messageSerializer = new MessageSerializer(); + String json = messageSerializer.serialize(jmsg).toString(); + logger.info(json); + Message message = new Message.Builder().addData("message", json).build(); + try { + MulticastResult result = GCMSender.send(message, regids, 3); + List<Result> results = result.getResults(); + for (int i = 0; i < results.size(); i++) { + logger.info("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"); + } + } else { + logger.info("GMS: no recipients"); + } + + /*** WinPhone ***/ + final List<String> urls = new ArrayList<>(); + if (isPM) { + PushQueries.getWinPhoneURL(sql, pmTo).ifPresent(urls::add); + } else { + List<Integer> uids = subscribedUsers.stream().map(com.juick.User::getUID).collect(Collectors.toList()); + if (uids.size() > 0) { + urls.addAll(PushQueries.getWindowsTokens(sql, uids)); + } + } + + + if (urls.isEmpty()) { + logger.info("WNS: no recipients"); + } else { + try { + String wnsToken = getWnsAccessToken(); + String text1 = "@" + jmsg.getUser().getUName(); + if (!jmsg.Tags.isEmpty()) { + text1 += ":" + XmlUtils.escape(jmsg.getTagsString()); + } + String text2 = XmlUtils.escape(jmsg.getText()); + String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + + "<toast>" + + "<visual>" + + "<binding template=\"ToastImageAndText02\">" + + "<image id=\"1\" src=\"http://i.juick.com/as/" + jmsg.getUser().getUID() + ".png\" />" + + "<text id=\"1\">" + text1 + "</text>" + + "<text id=\"2\">" + text2 + "</text>" + + "</binding>" + + "</visual>" + + "<commands>" + + "<command arguments=\"?mid=" + jmsg.getMID() + "\" />" + + "</commands>" + + "</toast>"; + logger.fine(xml); + for (String url : urls) { + logger.info("WNS: " + url); + sendWNS(wnsToken, url, xml); + } + } catch (IOException | IllegalStateException e) { + logger.log(Level.SEVERE, "WNS: ", e); + } + } + + /*** iOS ***/ + final List<String> tokens = new ArrayList<>(); + if (isPM) { + PushQueries.getAPNSToken(sql, pmTo).ifPresent(tokens::add); + } else { + List<Integer> uids = subscribedUsers.stream().map(com.juick.User::getUID).collect(Collectors.toList()); + if (uids.size() > 0) { + tokens.addAll(PushQueries.getAPNSTokens(sql, uids)); + } + } + 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.getUser().getUName()).alertBody(jmsg.getText()).build(); + logger.info("APNS: " + token); + service.push(token, payload); + } + } else { + logger.info("APNS: no recipients"); + } + } + + 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<NameValuePair> 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); + JSONObject json = new JSONObject(responseContent); + if(statusCode != 200) { + throw new IOException(json.opt("error") + ": " + json.opt("error_description")); + } + String tokenType = (String)json.get("token_type"); + if(tokenType.length() >= 1) { + tokenType = Character.toUpperCase(tokenType.charAt(0)) + tokenType.substring(1); + } + return tokenType + " " + json.get("access_token"); + } + + void sendWNS(String wnsToken, String url, 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(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); + } +} |