/* * 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.juick.Tag; import com.juick.service.CrosspostService; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.util.Assert; import rocks.xmpp.core.XmppException; import rocks.xmpp.core.session.Extension; import rocks.xmpp.core.session.XmppSession; import rocks.xmpp.core.session.XmppSessionConfiguration; import rocks.xmpp.core.stanza.model.Message; import rocks.xmpp.extensions.component.accept.ExternalComponent; import javax.annotation.PostConstruct; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.HttpsURLConnection; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.UUID; /** * @author Ugnich Anton */ public class Crosspost implements AutoCloseable { final static String TWITTERURL = "https://api.twitter.com/1.1/statuses/update.json"; final static String FBURL = "https://graph.facebook.com/me/feed"; final static String VKURL = "https://api.vk.com/method/wall.post"; private static Logger logger = LoggerFactory.getLogger(Crosspost.class); private final CrosspostService crosspostService; private XmppSession xmpp; private final String twitter_consumer_key; private final String twitter_consumer_secret; private final String crosspostJid; private final String password; private final int port; public Crosspost(final Environment env, final CrosspostService crosspostService) { Assert.notNull(env); Assert.notNull(crosspostService); this.crosspostService = crosspostService; twitter_consumer_key = env.getProperty("twitter_consumer_key", StringUtils.EMPTY); twitter_consumer_secret = env.getProperty("twitter_consumer_secret", StringUtils.EMPTY); crosspostJid = env.getProperty("crosspost_jid", "crosspost.juick.local"); password = env.getProperty("xmpp_password", StringUtils.EMPTY); port = NumberUtils.toInt(env.getProperty("xmpp_port"), 5347); } public static String percentEncode(final String s) { String ret = StringUtils.EMPTY; try { ret = URLEncoder.encode(s, CharEncoding.UTF_8).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); } catch (UnsupportedEncodingException e) { } return ret; } @PostConstruct public void init() { XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() .extensions(Extension.of(com.juick.Message.class)) .build(); xmpp = ExternalComponent.create(crosspostJid, password, configuration, "localhost", port); xmpp.addInboundMessageListener(e -> { Message msg = e.getMessage(); com.juick.Message jmsg = msg.getExtension(com.juick.Message.class); if (msg.getTo() != null && msg.getTo().getLocal() != null && jmsg != null && jmsg.getRid() == 0) { if (msg.getTo().getLocal().equals("twitter")) { twitterPost(jmsg); } else if (msg.getTo().getLocal().equals("fb")) { facebookPost(jmsg); } else if (msg.getTo().getLocal().equals("vk")) { vkontaktePost(jmsg); } } }); try { xmpp.connect(); } catch (XmppException e) { logger.warn("xmpp exception", e); } } @Override public void close() throws Exception { if (xmpp != null) xmpp.close(); logger.info("XmppSession on crosspost destroyed"); } public boolean facebookPost(final com.juick.Message jmsg) { String token = crosspostService.getFacebookToken(jmsg.getUser().getUid()).orElse(StringUtils.EMPTY); 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, CharEncoding.UTF_8) + "&message=" + URLEncoder.encode(status, CharEncoding.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 = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); conn.disconnect(); } catch (Exception e) { logger.error("fbPost exception", e); } return ret; } public boolean vkontaktePost(final com.juick.Message jmsg) { Pair tokens = crosspostService.getVkTokens(jmsg.getUser().getUid()).orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)); 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(), CharEncoding.UTF_8) + "&from_group=1&message=" + URLEncoder.encode(status, CharEncoding.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 = StringUtils.isNotEmpty(IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8)); conn.disconnect(); } catch (Exception e) { logger.error("vkPost exception", e); } return ret; } public boolean twitterPost(final com.juick.Message jmsg) { Pair tokens = crosspostService.getTwitterTokens(jmsg.getUser().getUid()).orElse(Pair.of(StringUtils.EMPTY, StringUtils.EMPTY)); 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 = IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8) != null; conn.disconnect(); } catch (Exception e) { logger.error("twitterPost exception", e); } return ret; } public String twitterSignature(final String status, final String nonce, final String timestamp, final String token, final 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.error("twitterSignature exception", e); } return null; } public String getMessageHashTags(final com.juick.Message jmsg) { String hashtags = StringUtils.EMPTY; for (Tag tag : jmsg.getTags()) { hashtags += "#" + tag + " "; } return hashtags; } }