Package com.oasisfeng.x3.gplusync

Source Code of com.oasisfeng.x3.gplusync.GPlusync

package com.oasisfeng.x3.gplusync;

import java.io.EOFException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import repacked.sun.misc.BASE64Encoder;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.conf.ConfigurationBuilder;

import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.oasisfeng.x3.xmpp.Notifier;

/** @author Oasis */
public class GPlusync extends HttpServlet {

  protected boolean extractURLWithoutProtocol = true;

  private static final Properties mConfig = new Properties();
  private static final URL KWeiboUpdateApiUrl;

  static {
    try { mConfig.load(GPlusync.class.getResourceAsStream("/gplusync.properties")); }
    catch (final IOException e) { throw new RuntimeException(e); }

    URL url; try { url = new URL("http://api.t.sina.com.cn/statuses/update.xml"); }
    catch (final MalformedURLException e) { throw new Error(e); }
    KWeiboUpdateApiUrl = url;
  }

  private static final String KSenderUserName = mConfig.getProperty("google.plus.user.nick") + " (Google+)";

  private static final int KMaxTweetLength = 140;
  private static final int KMaxWeiboTextLength = 280;
  private static final int KTwitterShortLinkLength = "https://t.co/12345678".length();
  private static final int KWeiboShortLinkLength = "http://t.cn/1234567".length();
  private static final String KTruncationTail = "… ";

  @Override
  public void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
    final Properties props = new Properties();
    final Session session = Session.getDefaultInstance(props, null);
    MimeMessage message;
    try {
      message = new MimeMessage(session, req.getInputStream());
      final Address[] from = message.getFrom();
      final Address[] recipients = message.getAllRecipients();
      if (from == null || from.length == 0 || recipients == null || recipients.length == 0) return;
      final InternetAddress email = (InternetAddress) from[0];
      final String recipient = ((InternetAddress) recipients[0]).getAddress();

      logMail(email, recipient, message);

      final String personal = email.getPersonal();
      if (personal == null || recipient == null || ! personal.equals(KSenderUserName)) return;

      String text = extractText(message);
      text = removeExplicitMentions(text);

      if (recipient.startsWith("plusync@")) {
        sendToTwitter(text, message);
        sendToWeibo(text, message);
      } else if (recipient.startsWith("plusync+twitter@"))
        sendToTwitter(text, message);
      else if (recipient.startsWith("plusync+weibo@"))
        sendToWeibo(text, message);
      else
        Notifier.send("Unknown recipient: " + recipient);
    } catch (final MessagingException e) {
      log.log(Level.SEVERE, "Error in parsing mail", e);
    }
  }

  private static final String KPlusSinaWeibo = "+Sina Weibo";
  private String removeExplicitMentions(String text) {
    text = text.trim();
    if (text.startsWith(KPlusSinaWeibo))
      text = text.substring(KPlusSinaWeibo.length()).trim();
    if (text.endsWith(KPlusSinaWeibo))
      text = text.substring(0, text.length() - KPlusSinaWeibo.length()).trim();
    return text;
  }

  private void sendToWeibo(final String aText, final MimeMessage aMessage) throws MalformedURLException,
      UnsupportedEncodingException, MessagingException, IOException {
    // Trim the text to the limit of weibo: 140 chars (as GBK)
    String text = aText;
    byte[] bytes = text.getBytes("GBK");
    if (bytes.length - calcLengthReductionWithURLsShortened(text) > KMaxWeiboTextLength) {
      final String link = extractLink(aMessage);
      if (link == null) throw new IllegalArgumentException("Malformed notification mail");
      bytes = Arrays.copyOfRange(bytes, 0, KMaxWeiboTextLength - KWeiboShortLinkLength - KTruncationTail.length());
      text = new String(bytes, "GBK") + KTruncationTail + link;
      Notifier.send("Truncated weibo: " + text);
    }

    if (text.startsWith("[test]")) return;

    final HTTPRequest request = new HTTPRequest(KWeiboUpdateApiUrl, HTTPMethod.POST);
    request.addHeader(new HTTPHeader("Authorization", "Basic " + iBase64.encode(iWeiboCredential.getBytes("US-ASCII"))));
    request.setPayload(("source=" + mConfig.getProperty("weibo.app.key") + "&status=" + URLEncoder.encode(text, "UTF-8")).getBytes("US-ASCII"));
    final HTTPResponse result = iURLFetch.fetch(request);
    if (result.getResponseCode() != 200) {
      final String msg = "Failed to post to Weibo: " + result.getResponseCode() + "\n" + new String(result.getContent(), "UTF-8");
      log.warning(msg);
      Notifier.send(msg);
    } else
      Notifier.send("To Weibo: " + text);
  }

  private void sendToTwitter(final String aText, final MimeMessage aMessage) throws MessagingException, IOException {
    String text = aText; //Normalizer.normalize(aText, Form.NFC);
    if (text.length() - calcLengthReductionWithURLsShortened(text) > KMaxTweetLength) {
        final String link = extractLink(aMessage);
      if (link == null) throw new IllegalArgumentException("Malformed notification mail");
      text = text.substring(0, KMaxTweetLength - KTwitterShortLinkLength - KTruncationTail.length());
      text += KTruncationTail + link;
      Notifier.send("Truncated tweet: " + text);
    }

    if (text.startsWith("[test]")) return;

    try {
      iTwitter.updateStatus(text);
      Notifier.send("To Twitter: " + text);
    } catch (final TwitterException e) {
      log.log(Level.WARNING, "Failed to post to Twitter", e);
      Notifier.send("Failed to post to Twitter due to " + e);
    }
  }

  /** Extract full text from Google+ notification mail */
  private String extractText(final MimeMessage message) throws MessagingException, IOException {
    final Multipart parts = (Multipart) message.getContent();
    for (int num_parts = parts.getCount(), i = 0; i < num_parts; i ++) {
      final BodyPart part = parts.getBodyPart(i);
      final String content_type = part.getContentType();
      if (! content_type.startsWith("text/plain")) continue;
      final Object content = part.getContent();
      if (! (content instanceof String)) continue;
      final Matcher matcher = iTextPattern.matcher((String) content);
      if (! matcher.find()) {
        log.warning("Text patten not found in body part " + i + ": " + content);
        continue;
      }
      final String text = matcher.group(1);
      if (text == null) continue;
      // Trim all leading white spaces in every line.
      final String combined = text.replaceAll(" \\r\\n", "").replace("\\r\\n", " ");
      log.info(combined);
      return combined;
    }
    throw new EOFException();
  }

  private String extractLink(final MimeMessage message) throws MessagingException, IOException {
    final Multipart parts = (Multipart) message.getContent();
    for (int num_parts = parts.getCount(), i = 0; i < num_parts; i ++) {
      final BodyPart part = parts.getBodyPart(i);
      final String content_type = part.getContentType();
      if (! content_type.startsWith("text/plain")) continue;
      final Object content = part.getContent();
      if (! (content instanceof String)) continue;
      final Matcher matcher = iLinkPattern.matcher((String) content);
      if (! matcher.find()) {
        log.warning("Link patten not found in body part " + i + ": " + content);
        continue;
      }
      final String link_path = matcher.group(1);
      if (link_path == null) continue;
      final String link = "https://plus.google.com/u/0" + URLDecoder.decode(link_path, "UTF-8");
      log.info(link);
      return link;
    }
    return null;
  }

  /** Calculate length of shortened content with wrapped URLs. */
  private int calcLengthReductionWithURLsShortened(final String text) {
    int length_reduction = 0;
    for (final String url : extractUnwrappedURLs(text))
      length_reduction += url.length() - KTwitterShortLinkLength;
    return length_reduction;
  }

  /** Borrowed from project https://github.com/twitter/twitter-text-java with minor modifications */
  private List<String> extractUnwrappedURLs(final String text) {
    if (text == null || text.isEmpty() || (extractURLWithoutProtocol ? text.indexOf('.') : text.indexOf(':')) == -1) {
      // Performance optimization.
      // If text doesn't contain '.' or ':' at all, text doesn't contain URL,
      // so we can simply return an empty list.
      return Collections.emptyList();
    }
    final List<String> urls = new ArrayList<String>();
    final Matcher matcher = Regex.VALID_URL.matcher(text);
    while (matcher.find()) {
      if (matcher.group(Regex.VALID_URL_GROUP_PROTOCOL) == null)
        // skip if protocol is not present and 'extractURLWithoutProtocol' is false
        // or URL is preceded by invalid character.
        if (!extractURLWithoutProtocol
            || Regex.INVALID_URL_WITHOUT_PROTOCOL_MATCH_BEGIN.matcher(
                matcher.group(Regex.VALID_URL_GROUP_BEFORE)).matches())
          continue;
      final String url = matcher.group(Regex.VALID_URL_GROUP_URL);
      final Matcher tco_matcher = Regex.VALID_TCO_URL.matcher(url);
      if (tco_matcher.find()) continue// Skip already wrapped URLs
      urls.add(url);
    }
    return urls;
  }

  private void logMail(final InternetAddress aFrom, final String aRecipient, final MimeMessage message) throws MessagingException, IOException {
    log.info("Mail from \"" + aFrom.getPersonal() + " <" + aFrom.getAddress() + ">\" to " + aRecipient + "\nSubject: " + message.getSubject());
    final Object content = message.getContent();
    if (content instanceof String)
      log.info("Mail content: " + (String) content);
    else if (content instanceof Multipart) {
      final Multipart multiparts = (Multipart) content;
      for (int num_parts = multiparts.getCount(), i = 0; i < num_parts; i ++) {
        final BodyPart part = multiparts.getBodyPart(i);
        log.info("Part " + i + " (" + part.getContentType() + "): " + part.getContent());
      }
    } else
      log.warning("Unknown content type: " + message.getContentType() + " (" + content.getClass().getSimpleName() + ")");
  }

  private static final String iWeiboCredential = mConfig.getProperty("weibo.username") + ":" + mConfig.getProperty("weibo.password");
  private static final URLFetchService iURLFetch = URLFetchServiceFactory.getURLFetchService();
  private static final BASE64Encoder iBase64 = new BASE64Encoder();
  /** Pattern for extracting post text from an Google+ notification mail body */
  private static final Pattern iTextPattern = Pattern.compile("^ *\"(.*)\" *$", Pattern.MULTILINE | Pattern.DOTALL);
  private static final Pattern iLinkPattern = Pattern.compile("://plus\\.google\\.com/.*&path=([0-9a-zA-Z%]*)&.*");
  private static final Twitter iTwitter = new TwitterFactory(new ConfigurationBuilder()
      .setOAuthConsumerKey(mConfig.getProperty("twitter.consumer.key"))
      .setOAuthConsumerSecret(mConfig.getProperty("twitter.consumer.secret"))
      .setOAuthAccessToken(mConfig.getProperty("twitter.access.token"))
      .setOAuthAccessTokenSecret(mConfig.getProperty("twitter.access.secret")).build()).getInstance();
  private static final Logger log = Logger.getLogger(GPlusync.class.getName());
  private static final long serialVersionUID = 1L;
}
TOP

Related Classes of com.oasisfeng.x3.gplusync.GPlusync

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.