com.vaushell.superpipes.tools.scribe.twitter.TwitterClient.java Source code

Java tutorial

Introduction

Here is the source code for com.vaushell.superpipes.tools.scribe.twitter.TwitterClient.java

Source

/*
 * Copyright (C) 2013 Fabien Vauchelles (fabien_AT_vauchelles_DOT_com).
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3, 29 June 2007, of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */

package com.vaushell.superpipes.tools.scribe.twitter;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vaushell.superpipes.tools.scribe.OAuthClient;
import com.vaushell.superpipes.tools.scribe.OAuthException;
import com.vaushell.superpipes.tools.scribe.code.A_ValidatorCode;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.scribe.builder.api.TwitterApi;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Verb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Twitter client.
 *
 * @author Fabien Vauchelles (fabien_AT_vauchelles_DOT_com)
 */
public class TwitterClient extends OAuthClient {
    // PUBLIC
    public static final int TWEET_SIZE = 140;
    public static final int TWEET_IMAGE_SIZE = 112;

    public TwitterClient() {
        super();

        this.fmt = DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy");
    }

    /**
     * Log in.
     *
     * @param key OAuth key
     * @param secret OAuth secret
     * @param tokenPath Path to save the token
     * @param vCode How to get the verification code
     * @throws IOException
     * @throws java.lang.InterruptedException
     */
    public void login(final String key, final String secret, final Path tokenPath, final A_ValidatorCode vCode)
            throws IOException, InterruptedException {
        loginImpl(TwitterApi.SSL.class, key, secret, null, null, true, tokenPath, vCode);
    }

    /**
     * Tweet message.
     *
     * @param message Tweet's content
     * @return Tweet's ID
     * @throws IOException
     * @throws OAuthException
     */
    public long tweet(final String message) throws IOException, OAuthException {
        if (message == null) {
            throw new IllegalArgumentException();
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] tweet() : message=" + message);
        }

        final OAuthRequest request = new OAuthRequest(Verb.POST,
                "https://api.twitter.com/1.1/statuses/update.json");
        request.addBodyParameter("status", message);

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode node = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, node);

        return node.get("id").asLong();
    }

    /**
     * Tweet picture.
     *
     * @param message Tweet's content
     * @param picturePath Path of the picture
     * @return Tweet's ID
     * @throws IOException
     * @throws OAuthException
     */
    public long tweetPicture(final String message, final Path picturePath) throws IOException, OAuthException {
        if (picturePath == null || Files.notExists(picturePath)) {
            throw new IllegalArgumentException();
        }

        // Don't use a try-with-resources
        // Because of a findbug bug on : RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE
        // double check in the finally
        InputStream is = null;
        try {
            is = Files.newInputStream(picturePath);

            return tweetPicture(message, is);
        } finally {
            if (null != is) {
                is.close();
            }
        }
    }

    /**
     * Tweet picture.
     *
     * @param message Tweet's content
     * @param is InputStream of the picture
     * @return Tweet's ID
     * @throws IOException
     * @throws OAuthException
     */
    public long tweetPicture(final String message, final InputStream is) throws IOException, OAuthException {
        if (message == null || is == null) {
            throw new IllegalArgumentException();
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] tweetPicture() : message=" + message);
        }

        final OAuthRequest request = new OAuthRequest(Verb.POST,
                "https://api.twitter.com/1.1/statuses/update_with_media.json");
        final HttpEntity entity = MultipartEntityBuilder.create().addBinaryBody("status", message.getBytes("UTF-8"))
                .addBinaryBody("media[]", is, ContentType.APPLICATION_OCTET_STREAM, "media").build();

        final Header contentType = entity.getContentType();
        request.addHeader(contentType.getName(), contentType.getValue());

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            entity.writeTo(bos);
            request.addPayload(bos.toByteArray());
        }

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode node = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, node);

        return node.get("id").asLong();
    }

    /**
     * Read a tweet.
     *
     * @param ID Tweet ID
     * @return the tweet
     * @throws IOException
     * @throws com.vaushell.superpipes.tools.scribe.OAuthException
     */
    public TW_Tweet readTweet(final long ID) throws IOException, OAuthException {
        if (ID < 0) {
            throw new IllegalArgumentException();
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] readTweet() : ID=" + ID);
        }

        final OAuthRequest request = new OAuthRequest(Verb.GET,
                "https://api.twitter.com/1.1/statuses/show.json?id=" + Long.toString(ID));

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode node = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, node);

        return convertJsonToTweet(node);
    }

    /**
     * Delete a tweet.
     *
     * @param ID Tweet ID
     * @return True if successfull
     * @throws IOException
     * @throws com.vaushell.superpipes.tools.scribe.OAuthException
     */
    public boolean deleteTweet(final long ID) throws IOException, OAuthException {
        if (ID < 0) {
            throw new IllegalArgumentException();
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] deleteTweet() : ID=" + ID);
        }

        final OAuthRequest request = new OAuthRequest(Verb.POST,
                "https://api.twitter.com/1.1/statuses/destroy/" + ID + ".json");

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode node = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, node);

        final JsonNode nodeID = node.get("id");
        if (nodeID == null) {
            return false;
        } else {
            return ID == nodeID.asLong();
        }
    }

    /**
     * Read a Twitter timeline.
     *
     * @param forcedTarget Target's ID. Could be null to use login target.
     * @param count Max tweet. Could be null to use default.
     * @return a list of tweets
     * @throws IOException
     * @throws OAuthException
     */
    public List<TW_Tweet> readTimeline(final Long forcedTarget, final Integer count)
            throws IOException, OAuthException {
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] readTimeline() : forcedTarget=" + forcedTarget
                    + " / count=" + count);
        }

        final String url;
        final Properties properties = new Properties();
        if (forcedTarget == null) {
            url = "https://api.twitter.com/1.1/statuses/home_timeline.json";
        } else {
            url = "https://api.twitter.com/1.1/statuses/user_timeline.json";
            properties.setProperty("user_id", Long.toString(forcedTarget));
        }
        if (count != null) {
            properties.setProperty("count", Integer.toString(count));
        }

        return readTimelineImpl(url, properties);
    }

    /**
     * Iterate a Twitter timeline.
     *
     * @param forcedTarget Target's ID. Could be null to use login target.
     * @param count Max tweet by call. Could be null to use default.
     * @return a tweets iterator
     */
    public Iterator<TW_Tweet> iteratorTimeline(final Long forcedTarget, final Integer count) {
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] iteratorTimeline() : forcedTarget=" + forcedTarget
                    + " / count=" + count);
        }

        return new Iterator<TW_Tweet>() {
            @Override
            public boolean hasNext() {
                try {
                    if (bufferCursor < buffer.size()) {
                        return true;
                    } else {
                        buffer.clear();
                        bufferCursor = 0;

                        final String url;
                        final Properties properties = new Properties();
                        if (forcedTarget == null) {
                            url = "https://api.twitter.com/1.1/statuses/home_timeline.json";
                        } else {
                            url = "https://api.twitter.com/1.1/statuses/user_timeline.json";
                            properties.setProperty("user_id", Long.toString(forcedTarget));
                        }
                        if (count != null) {
                            properties.setProperty("count", Integer.toString(count));
                        }
                        if (maxID != null) {
                            properties.setProperty("max_id", Long.toString(maxID - 1L));
                        }

                        final List<TW_Tweet> tweets = readTimelineImpl(url, properties);
                        if (tweets.isEmpty()) {
                            return false;
                        } else {
                            maxID = tweets.get(tweets.size() - 1).getID();

                            buffer.addAll(tweets);

                            return true;
                        }
                    }
                } catch (final OAuthException | IOException ex) {
                    throw new RuntimeException(ex);
                }
            }

            @Override
            public TW_Tweet next() {
                return buffer.get(bufferCursor++);
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }

            // PRIVATE
            private final List<TW_Tweet> buffer = new ArrayList<>();
            private int bufferCursor;
            private Long maxID;
        };
    }

    /**
     * Retweet a tweet.
     *
     * @param ID Tweet's ID
     * @return Retweet's ID
     * @throws IOException
     * @throws OAuthException
     */
    public long retweet(final long ID) throws IOException, OAuthException {
        if (ID < 0) {
            throw new IllegalArgumentException();
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("[" + getClass().getSimpleName() + "] retweet() : ID=" + ID);
        }

        final OAuthRequest request = new OAuthRequest(Verb.POST,
                "https://api.twitter.com/1.1/statuses/retweet/" + Long.toString(ID) + ".json");

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode node = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, node);

        return node.get("id").asLong();
    }

    // PRIVATE
    private static final Logger LOGGER = LoggerFactory.getLogger(TwitterClient.class);
    private final DateTimeFormatter fmt;

    private void checkErrors(final Response response, final JsonNode root) throws OAuthException {
        final JsonNode error = root.get("errors");
        if (error != null) {
            final JsonNode first = error.get(0);

            throw new OAuthException(response.getCode(), first.get("code").asInt(), first.get("message").asText());
        }
    }

    private List<TW_Tweet> readTimelineImpl(final String url, final Properties properties)
            throws IOException, OAuthException {
        if (url == null || properties == null) {
            throw new IllegalArgumentException();
        }

        final OAuthRequest request = new OAuthRequest(Verb.GET, url);
        for (final Entry<Object, Object> entry : properties.entrySet()) {
            request.addQuerystringParameter((String) entry.getKey(), (String) entry.getValue());
        }

        final Response response = sendSignedRequest(request);

        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode nodes = (JsonNode) mapper.readTree(response.getStream());

        checkErrors(response, nodes);

        final List<TW_Tweet> tweets = new ArrayList<>();
        for (final JsonNode node : nodes) {
            tweets.add(convertJsonToTweet(node));
        }

        return tweets;
    }

    private TW_Tweet convertJsonToTweet(final JsonNode node) {
        // Replace shorten URLs with expanded URLs
        String text = node.get("text").asText();

        final JsonNode nodeEntities = node.get("entities");
        if (nodeEntities != null) {
            final JsonNode nodeUrls = nodeEntities.get("urls");
            if (nodeUrls != null) {
                for (final JsonNode nodeUrl : nodeUrls) {
                    text = text.replace(nodeUrl.get("url").asText(), nodeUrl.get("expanded_url").asText());
                }
            }
        }

        final JsonNode nodeUser = node.get("user");

        return new TW_Tweet(
                node.get("id").asLong(), text, new TW_User(nodeUser.get("id").asLong(),
                        nodeUser.get("name").asText(), nodeUser.get("screen_name").asText()),
                fmt.parseDateTime(node.get("created_at").asText()));
    }
}