diff options
Diffstat (limited to 'juick-api/src/main/java/com/juick/components')
3 files changed, 1108 insertions, 0 deletions
diff --git a/juick-api/src/main/java/com/juick/components/CrosspostComponent.java b/juick-api/src/main/java/com/juick/components/CrosspostComponent.java new file mode 100644 index 00000000..b0e55719 --- /dev/null +++ b/juick-api/src/main/java/com/juick/components/CrosspostComponent.java @@ -0,0 +1,323 @@ +/* + * 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.components; + +import com.juick.server.CrosspostQueries; +import com.juick.xmpp.JID; +import com.juick.xmpp.Message; +import com.juick.xmpp.Stream; +import com.juick.xmpp.StreamComponent; +import com.juick.xmpp.extensions.JuickMessage; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.*; +import java.net.Socket; +import java.net.URL; +import java.net.URLEncoder; +import java.security.Key; +import java.util.Properties; +import java.util.UUID; +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; + +/** + * + * @author Ugnich Anton + */ +public class CrosspostComponent implements ServletContextListener, Stream.StreamListener, Message.MessageListener { + + private static Logger logger = Logger.getLogger(CrosspostComponent.class.getName()); + + private ExecutorService executorService; + + public final static String TWITTERURL = "https://api.twitter.com/1.1/statuses/update.json"; + public final static String FBURL = "https://graph.facebook.com/me/feed"; + public final static String VKURL = "https://api.vk.com/method/wall.post"; + JdbcTemplate sql; + Stream xmpp; + String twitter_consumer_key; + String twitter_consumer_secret; + + @Override + public void contextInitialized(final ServletContextEvent sce) { + logger.info("component initialized"); + executorService = Executors.newSingleThreadExecutor(); + executorService.submit((Runnable) () -> { + try { + Properties conf = new Properties(); + conf.load(sce.getServletContext().getResourceAsStream("/WEB-INF/juick.conf")); + + LogManager.getLogManager().readConfiguration( + sce.getServletContext().getResourceAsStream("/WEB-INF/logging.properties")); + twitter_consumer_key = conf.getProperty("twitter_consumer_key", ""); + twitter_consumer_secret = conf.getProperty("twitter_consumer_secret", ""); + + setupSql(conf.getProperty("datasource_driver", "com.mysql.jdbc.Driver"), conf.getProperty("datasource_url", "")); + setupXmppComponent(conf.getProperty("xmpp_password", "")); + } catch (Exception 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(String password) { + try { + Socket socket = new Socket("localhost", 5347); + xmpp = new StreamComponent(new JID("", "crosspost.juick.com", ""), socket.getInputStream(), socket.getOutputStream(), password); + xmpp.addChildParser(new JuickMessage()); + xmpp.addListener((Stream.StreamListener) this); + xmpp.addListener((Message.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); + if (msg.to != null && msg.to.Username != null && jmsg != null && jmsg.getRID() == 0) { + if (msg.to.Username.equals("twitter")) { + twitterPost(jmsg); + } else if (msg.to.Username.equals("fb")) { + facebookPost(jmsg); + } else if (msg.to.Username.equals("vk")) { + vkontaktePost(jmsg); + } + } + } + + public boolean facebookPost(com.juick.Message jmsg) { + String token = CrosspostQueries.getFacebookToken(sql, jmsg.getUser().getUID()).orElse(""); + if (token.isEmpty()) { + return false; + } + + logger.info("FB: #" + jmsg.getMID()); + + String status = getMessageHashTags(jmsg) + "\n" + jmsg.getText(); + + boolean ret = false; + try { + String body = "access_token=" + URLEncoder.encode(token, "UTF-8") + "&message=" + URLEncoder.encode(status, "UTF-8") + "&link=http%3A%2F%2Fjuick.com%2F" + jmsg.getMID(); + + HttpsURLConnection conn = (HttpsURLConnection) new URL(FBURL).openConnection(); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("User-Agent", "Juick"); + conn.setRequestProperty("Content-Length", Integer.toString(body.length())); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.connect(); + + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write(body); + wr.close(); + + ret = streamToString(conn.getInputStream()) != null; + + conn.disconnect(); + } catch (Exception e) { + logger.log(Level.SEVERE, "fbPost: " + e.getMessage(), e); + } + return ret; + } + + public boolean vkontaktePost(com.juick.Message jmsg) { + Pair<String, String> tokens = CrosspostQueries.getVKTokens(sql, jmsg.getUser().getUID()).orElse(Pair.of("", "")); + if (tokens.getLeft().isEmpty() || tokens.getRight().isEmpty()) { + return false; + } + + logger.info("VK: #" + jmsg.getMID()); + + String status = getMessageHashTags(jmsg) + "\n" + jmsg.getText() + "\nhttp://juick.com/" + jmsg.getMID(); + + boolean ret = false; + try { + String body = "owner_id=" + tokens.getLeft() + "&access_token=" + URLEncoder.encode(tokens.getRight(), "UTF-8") + "&from_group=1&message=" + URLEncoder.encode(status, "UTF-8"); + + HttpsURLConnection conn = (HttpsURLConnection) new URL(VKURL).openConnection(); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("User-Agent", "Juick"); + conn.setRequestProperty("Content-Length", Integer.toString(body.length())); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.connect(); + + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write(body); + wr.close(); + + ret = streamToString(conn.getInputStream()) != null; + + conn.disconnect(); + } catch (Exception e) { + logger.log(Level.SEVERE, "vkPost: " + e.getMessage(), e); + } + return ret; + } + + public boolean twitterPost(com.juick.Message jmsg) { + Pair<String, String> tokens = CrosspostQueries.getTwitterTokens(sql, jmsg.getUser().getUID()).orElse(Pair.of("", "")); + if (tokens.getLeft().isEmpty() || tokens.getRight().isEmpty()) { + return false; + } + String token = percentEncode(tokens.getLeft()); + String token_secret = percentEncode(tokens.getRight()); + + logger.info("TWITTER: #" + jmsg.getMID()); + + String status = getMessageHashTags(jmsg) + jmsg.getText(); + if (status.length() > 115) { + status = status.substring(0, 114) + "…"; + } + status += " http://juick.com/" + jmsg.getMID(); + status = percentEncode(status); + + boolean ret = false; + try { + String nonce = UUID.randomUUID().toString(); + String timestamp = Long.toString(System.currentTimeMillis() / 1000L); + String signature = percentEncode(twitterSignature(status, nonce, timestamp, token, token_secret)); + String auth = "OAuth " + + "oauth_consumer_key=\"" + twitter_consumer_key + "\", " + + "oauth_nonce=\"" + nonce + "\", " + + "oauth_signature=\"" + signature + "\", " + + "oauth_signature_method=\"HMAC-SHA1\", " + + "oauth_timestamp=\"" + timestamp + "\", " + + "oauth_token=\"" + token + "\", " + + "oauth_version=\"1.0\""; + + HttpsURLConnection conn = (HttpsURLConnection) new URL(TWITTERURL).openConnection(); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("User-Agent", "Juick"); + conn.setRequestProperty("Content-Length", Integer.toString(status.length() + 7)); + conn.setRequestProperty("Authorization", auth); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.connect(); + + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write("status=" + status); + wr.close(); + + ret = streamToString(conn.getInputStream()) != null; + + conn.disconnect(); + } catch (Exception e) { + logger.log(Level.SEVERE, "twitterPost: " + e.getMessage(), e); + } + return ret; + } + + public String twitterSignature(String status, String nonce, String timestamp, String token, String token_secret) { + try { + // ALPHABET-SORTED + String params = "oauth_consumer_key=" + twitter_consumer_key + + "&oauth_nonce=" + nonce + + "&oauth_signature_method=HMAC-SHA1" + + "&oauth_timestamp=" + timestamp + + "&oauth_token=" + token + + "&oauth_version=1.0" + + "&status=" + status; + + String base = "POST&" + percentEncode(TWITTERURL) + "&" + percentEncode(params); + String key = twitter_consumer_secret + "&" + token_secret; + + Key signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(signingKey); + byte[] rawHmac = mac.doFinal(base.getBytes()); + return Base64.encodeBase64String(rawHmac); + + } catch (Exception e) { + logger.log(Level.SEVERE, "twitterSignature: " + e.getMessage(), e); + } + return null; + } + + 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.log(Level.SEVERE, "streamToString: " + e.getMessage(), e); + } + return null; + } + + public String getMessageHashTags(com.juick.Message jmsg) { + String hashtags = ""; + for (int i = 0; i < jmsg.Tags.size(); i++) { + hashtags += "#" + jmsg.Tags.get(i) + " "; + } + return hashtags; + } + + public static String percentEncode(String s) { + String ret = ""; + try { + ret = URLEncoder.encode(s, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); + } catch (UnsupportedEncodingException e) { + } + return ret; + } +} diff --git a/juick-api/src/main/java/com/juick/components/PushComponent.java b/juick-api/src/main/java/com/juick/components/PushComponent.java new file mode 100644 index 00000000..baac2532 --- /dev/null +++ b/juick-api/src/main/java/com/juick/components/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.components; + +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(() -> { + 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) { + regids.addAll(PushQueries.getAndroidRegID(sql, pmTo)); + } 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) { + urls.addAll(PushQueries.getWinPhoneURL(sql, pmTo)); + } 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) { + tokens.addAll(PushQueries.getAPNSToken(sql, pmTo)); + } 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); + } +} diff --git a/juick-api/src/main/java/com/juick/components/XMPPComponent.java b/juick-api/src/main/java/com/juick/components/XMPPComponent.java new file mode 100644 index 00000000..7b31ac59 --- /dev/null +++ b/juick-api/src/main/java/com/juick/components/XMPPComponent.java @@ -0,0 +1,474 @@ +package com.juick.components; + +import com.juick.User; +import com.juick.server.MessagesQueries; +import com.juick.server.SubscriptionsQueries; +import com.juick.server.UserQueries; +import com.juick.xmpp.*; +import com.juick.xmpp.extensions.JuickMessage; +import com.juick.xmpp.extensions.Nickname; +import com.juick.xmpp.extensions.XOOB; +import com.juick.xmpp.s2s.*; +import org.apache.commons.dbcp2.BasicDataSource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.xmlpull.v1.XmlPullParserException; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author ugnich + */ +public class XMPPComponent implements ServletContextListener, Stream.StreamListener, + Message.MessageListener, Iq.IqListener, Presence.PresenceListener { + + private static final Logger logger = Logger.getLogger(XMPPComponent.class.getName()); + + public final ExecutorService executorService = Executors.newCachedThreadPool(); + private StreamComponent router; + JuickBot bot; + + public String HOSTNAME, componentName; + public String STATSFILE = null; + public String keystore; + public String keystorePassword; + public List<String> brokenSSLhosts; + public List<String> bannedHosts; + private final List<ConnectionIn> inConnections = Collections.synchronizedList(new ArrayList<>()); + private final List<ConnectionOut> outConnections = Collections.synchronizedList(new ArrayList<>()); + private final List<CacheEntry> outCache = Collections.synchronizedList(new ArrayList<>()); + private JdbcTemplate sql; + final public HashMap<String, StanzaChild> childParsers = new HashMap<>(); + + public void addConnectionIn(ConnectionIn c) { + synchronized (getInConnections()) { + getInConnections().add(c); + } + } + + public void addConnectionOut(ConnectionOut c) { + synchronized (getOutConnections()) { + getOutConnections().add(c); + } + } + + public void removeConnectionIn(ConnectionIn c) { + synchronized (getInConnections()) { + getInConnections().remove(c); + } + } + + public void removeConnectionOut(ConnectionOut c) { + synchronized (getOutConnections()) { + getOutConnections().remove(c); + } + } + + public String getFromCache(String hostname) { + CacheEntry ret = null; + synchronized (getOutCache()) { + for (Iterator<CacheEntry> i = getOutCache().iterator(); i.hasNext();) { + CacheEntry c = i.next(); + if (c.hostname != null && c.hostname.equals(hostname)) { + ret = c; + i.remove(); + break; + } + } + } + return (ret != null) ? ret.xml : null; + } + + public ConnectionOut getConnectionOut(String hostname, boolean needReady) { + synchronized (getOutConnections()) { + for (ConnectionOut c : getOutConnections()) { + if (c.to != null && c.to.equals(hostname) && (!needReady || c.streamReady)) { + return c; + } + } + } + return null; + } + + public ConnectionIn getConnectionIn(String streamID) { + synchronized (getInConnections()) { + for (ConnectionIn c : getInConnections()) { + if (c.streamID != null && c.streamID.equals(streamID)) { + return c; + } + } + } + return null; + } + + public void sendOut(Stanza s) { + sendOut(s.to.Host, s.toString()); + } + + public void sendOut(String hostname, String xml) { + boolean haveAnyConn = false; + + ConnectionOut connOut = null; + synchronized (getOutConnections()) { + for (ConnectionOut c : getOutConnections()) { + if (c.to != null && c.to.equals(hostname)) { + if (c.streamReady) { + connOut = c; + break; + } else { + haveAnyConn = true; + break; + } + } + } + } + if (connOut != null) { + try { + connOut.sendStanza(xml); + } catch (IOException e) { + logger.warning("STREAM TO " + connOut.to + " " + connOut.streamID + " ERROR: " + e.toString()); + } + return; + } + + boolean haveCache = false; + synchronized (getOutCache()) { + for (CacheEntry c : getOutCache()) { + if (c.hostname != null && c.hostname.equals(hostname)) { + c.xml += xml; + c.tsUpdated = System.currentTimeMillis(); + haveCache = true; + break; + } + } + if (!haveCache) { + getOutCache().add(new CacheEntry(hostname, xml)); + } + } + + if (!haveAnyConn) { + try { + ConnectionOut connectionOut = new ConnectionOut(this, hostname); + executorService.submit(connectionOut); + } catch (CertificateException | UnrecoverableKeyException | NoSuchAlgorithmException | XmlPullParserException | KeyStoreException | KeyManagementException | IOException e) { + logger.log(Level.SEVERE, "s2s out error", e); + } + } + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + + logger.info("component initialized"); + Properties conf = new Properties(); + try { + conf.load(sce.getServletContext().getResourceAsStream("/WEB-INF/juick.conf")); + HOSTNAME = conf.getProperty("hostname"); + componentName = conf.getProperty("componentname"); + JID Jid = new JID(conf.getProperty("xmppbot_jid")); + STATSFILE = conf.getProperty("statsfile"); + keystore = conf.getProperty("keystore"); + keystorePassword = conf.getProperty("keystore_password"); + brokenSSLhosts = Arrays.asList(conf.getProperty("broken_ssl_hosts", "").split(",")); + bannedHosts = Arrays.asList(conf.getProperty("banned_hosts", "").split(",")); + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(conf.getProperty("datasource_driver", "com.mysql.jdbc.Driver")); + dataSource.setUrl(conf.getProperty("datasource_url")); + setSql(new JdbcTemplate(dataSource)); + bot = new JuickBot(this, Jid); + + childParsers.put(JuickMessage.XMLNS, new JuickMessage()); + + executorService.submit(() -> { + Socket routerSocket = null; + try { + routerSocket = new Socket("localhost", 5347); + setRouter(new StreamComponent(new JID("s2s"), routerSocket.getInputStream(), routerSocket.getOutputStream(), conf.getProperty("xmpp_password"))); + getRouter().addChildParser(new JuickMessage()); + getRouter().addListener((Stream.StreamListener) this); + getRouter().addListener((Message.MessageListener) this); + getRouter().addListener((Iq.IqListener) this); + getRouter().startParsing(); + } catch (IOException e) { + logger.log(Level.SEVERE, "router error", e); + } + }); + executorService.submit(() -> { + final ServerSocket listener = new ServerSocket(5269); + logger.info("s2s listener ready"); + while (true) { + try { + Socket socket = listener.accept(); + ConnectionIn client = new ConnectionIn(this, bot, socket); + addConnectionIn(client); + executorService.submit(client); + } catch (Exception e) { + logger.log(Level.SEVERE, "s2s error", e); + } + } + }); + executorService.submit(new CleaningUp(this)); + + } catch (Exception e) { + logger.log(Level.SEVERE, "XMPPComponent error", e); + } + } + + + + @Override + public void contextDestroyed(ServletContextEvent sce) { + synchronized (getOutConnections()) { + for (Iterator<ConnectionOut> i = getOutConnections().iterator(); i.hasNext();) { + ConnectionOut c = i.next(); + c.closeConnection(); + i.remove(); + } + } + + synchronized (getInConnections()) { + for (Iterator<ConnectionIn> i = getInConnections().iterator(); i.hasNext();) { + ConnectionIn c = i.next(); + c.closeConnection(); + i.remove(); + } + } + + try { + closeRouterConnection(); + } catch (IOException e) { + logger.log(Level.WARNING, "router warning", e); + } + executorService.shutdown(); + logger.info("component destroyed"); + } + public void closeRouterConnection() throws IOException { + getRouter().logoff(); + } + + public void sendJuickMessage(JuickMessage jmsg) { + List<String> jids = new ArrayList<>(); + + if (jmsg.FriendsOnly) { + jids = SubscriptionsQueries.getJIDSubscribedToUser(getSql(), jmsg.getUser().getUID(), jmsg.FriendsOnly); + } else { + List<User> users = SubscriptionsQueries.getSubscribedUsers(getSql(), jmsg.getUser().getUID(), jmsg.getMID()); + for (User user : users) { + for (String jid : UserQueries.getJIDsbyUID(getSql(), user.getUID())) { + jids.add(jid); + } + } + } + + String txt = "@" + jmsg.getUser().getUName() + ":" + jmsg.getTagsString() + "\n"; + String attachment = jmsg.getAttachmentURL(); + if (attachment != null) { + txt += attachment + "\n"; + } + txt += jmsg.getText() + "\n\n"; + txt += "#" + jmsg.getMID() + " http://juick.com/" + jmsg.getMID(); + + Nickname nick = new Nickname(); + nick.Nickname = "@" + jmsg.getUser().getUName(); + + com.juick.xmpp.Message msg = new com.juick.xmpp.Message(); + msg.from = bot.JuickJID; + msg.body = txt; + msg.type = Message.Type.chat; + msg.thread = "juick-" + jmsg.getMID(); + msg.addChild(jmsg); + msg.addChild(nick); + if (attachment != null) { + XOOB oob = new XOOB(); + oob.URL = attachment; + msg.addChild(oob); + } + + for (String jid : jids) { + msg.to = new JID(jid); + sendOut(msg); + } + } + + public void sendJuickComment(JuickMessage jmsg) { + List<User> users; + String replyQuote; + String replyTo; + + users = SubscriptionsQueries.getUsersSubscribedToComments(getSql(), jmsg.getMID(), jmsg.getUser().getUID()); + com.juick.Message replyMessage = jmsg.ReplyTo > 0 ? MessagesQueries.getReply(getSql(), jmsg.getMID(), jmsg.ReplyTo) + : MessagesQueries.getMessage(getSql(), jmsg.getMID()); + replyTo = replyMessage.getUser().getUName(); + replyQuote = getReplyQuote(replyMessage); + + String txt = "Reply by @" + jmsg.getUser().getUName() + ":\n" + replyQuote + "\n@" + replyTo + " "; + String attachment = jmsg.getAttachmentURL(); + if (attachment != null) { + txt += attachment + "\n"; + } + txt += jmsg.getText() + "\n\n" + "#" + jmsg.getMID() + "/" + jmsg.getRID() + " http://juick.com/" + jmsg.getMID() + "#" + jmsg.getRID(); + + com.juick.xmpp.Message msg = new com.juick.xmpp.Message(); + msg.from = bot.JuickJID; + msg.body = txt; + msg.type = Message.Type.chat; + msg.addChild(jmsg); + for (User user : users) { + for (String jid : UserQueries.getJIDsbyUID(getSql(), user.getUID())) { + msg.to = new JID(jid); + sendOut(msg); + } + } + } + + private String getReplyQuote(com.juick.Message q) { + String quote = q.getText(); + if (quote.length() > 50) { + quote = ">" + quote.substring(0, 47).replace('\n', ' ') + "...\n"; + } else if (quote.length() > 0) { + quote = ">" + quote.replace('\n', ' ') + "\n"; + } + return quote; + } + + public void sendJuickRecommendation(JuickMessage recomm) { + List<User> users; + JuickMessage jmsg; + jmsg = new JuickMessage(MessagesQueries.getMessage(getSql(), recomm.getMID())); + users = SubscriptionsQueries.getUsersSubscribedToUserRecommendations(getSql(), + recomm.getUser().getUID(), recomm.getMID(), jmsg.getUser().getUID()); + + String txt = "Recommended by @" + recomm.getUser().getUName() + ":\n"; + txt += "@" + jmsg.getUser().getUName() + ":" + jmsg.getTagsString() + "\n"; + String attachment = jmsg.getAttachmentURL(); + if (attachment != null) { + txt += attachment + "\n"; + } + txt += jmsg.getText() + "\n\n"; + txt += "#" + jmsg.getMID(); + if (jmsg.Replies > 0) { + if (jmsg.Replies % 10 == 1 && jmsg.Replies % 100 != 11) { + txt += " (" + jmsg.Replies + " reply)"; + } else { + txt += " (" + jmsg.Replies + " replies)"; + } + } + txt += " http://juick.com/" + jmsg.getMID(); + + Nickname nick = new Nickname(); + nick.Nickname = "@" + jmsg.getUser().getUName(); + + com.juick.xmpp.Message msg = new com.juick.xmpp.Message(); + msg.from = bot.JuickJID; + msg.body = txt; + msg.type = Message.Type.chat; + msg.thread = "juick-" + jmsg.getMID(); + msg.addChild(jmsg); + msg.addChild(nick); + if (attachment != null) { + XOOB oob = new XOOB(); + oob.URL = attachment; + msg.addChild(oob); + } + + for (User user : users) { + for (String jid : UserQueries.getJIDsbyUID(getSql(), user.getUID())) { + msg.to = new JID(jid); + sendOut(msg); + } + } + } + + @Override + public boolean onIq(Iq iq) { + JID jid = iq.to; + if (!jid.Host.equals(componentName)) { + logger.info("STREAM ROUTER (IQ): " + iq.toString()); + sendOut(iq); + } + return false; + } + + @Override + public void onMessage(Message xmsg) { + logger.info("STREAM ROUTER (PROCESS): " + xmsg.toString()); + JuickMessage jmsg = (JuickMessage) xmsg.getChild(JuickMessage.XMLNS); + JID jid = xmsg.to; + if (jid.Host.equals(componentName)) { + if (jmsg != null) { + if (jid.Username != null && jid.Username.equals("recomm")) { + sendJuickRecommendation(jmsg); + } else { + if (jmsg.getRID() > 0) { + sendJuickComment(jmsg); + } else if (jmsg.getMID() > 0) { + sendJuickMessage(jmsg); + } + } + } + } else { + sendOut(xmsg); + } + } + + @Override + public void onPresence(Presence presence) { + JID jid = presence.to; + if (!jid.Host.equals(componentName)) { + logger.info("STREAM ROUTER (PRESENCE): " + presence.toString()); + sendOut(presence); + } + } + + @Override + public void onStreamReady() { + logger.info("STREAM ROUTER (READY)"); + } + + @Override + public void onStreamFail(Exception ex) { + logger.log(Level.SEVERE, "STREAM ROUTER (FAIL)", ex); + } + + public StreamComponent getRouter() { + return router; + } + + public void setRouter(StreamComponent router) { + this.router = router; + } + + public List<ConnectionIn> getInConnections() { + return inConnections; + } + + public List<ConnectionOut> getOutConnections() { + return outConnections; + } + + public List<CacheEntry> getOutCache() { + return outCache; + } + + public JdbcTemplate getSql() { + return sql; + } + + public void setSql(JdbcTemplate sql) { + this.sql = sql; + } +} |