org.loklak.harvester.TwitterAPI.java Source code

Java tutorial

Introduction

Here is the source code for org.loklak.harvester.TwitterAPI.java

Source

/**
 *  TwitterAPI
 *  Copyright 16.07.2015 by Michael Peter Christen, @0rb1t3r
 *
 *  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 2.1 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 program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package org.loklak.harvester;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.joda.time.DateTime;
import org.json.JSONArray;
import org.json.JSONObject;
import org.loklak.LoklakServer;
import org.loklak.data.DAO;
import org.loklak.geo.GeoMark;
import org.loklak.objects.AbstractObjectEntry;
import org.loklak.objects.AccountEntry;
import org.loklak.objects.UserEntry;
import org.loklak.tools.DateParser;
import org.loklak.tools.storage.JsonDataset;
import org.loklak.tools.storage.JsonFactory;
import org.loklak.tools.storage.JsonMinifier;
import org.loklak.tools.storage.JsonDataset.JsonFactoryIndex;

import com.google.common.base.Charsets;
import com.google.common.io.Files;

import twitter4j.IDs;
import twitter4j.RateLimitStatus;
import twitter4j.ResponseList;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.TwitterObjectFactory;
import twitter4j.User;
import twitter4j.conf.ConfigurationBuilder;

@SuppressWarnings("unused")
public class TwitterAPI {

    private final static String RATE_ACCOUNT_LOGIN_VERIFICATION_ENROLLMENT = "/account/login_verification_enrollment"; // limit = 15
    private final static String RATE_ACCOUNT_SETTINGS = "/account/settings"; // limit = 15
    private final static String RATE_ACCOUNT_UPDATE_PROFILE = "/account/update_profile"; // limit = 15
    private final static String RATE_ACCOUNT_VERIFY_CREDENTIALS = "/account/verify_credentials"; // limit = 15
    private final static String RATE_APPLICATION_RATE_LIMIT_STATUS = "/application/rate_limit_status"; // limit = 180
    private final static String RATE_BLOCKS_IDS = "/blocks/ids"; // limit = 15
    private final static String RATE_BLOCKS_LIST = "/blocks/list"; // limit = 15
    private final static String RATE_COLLECTIONS_ENTRIES = "/collections/entries"; // limit = 1000
    private final static String RATE_COLLECTIONS_LIST = "/collections/list"; // limit = 1000
    private final static String RATE_COLLECTIONS_SHOW = "/collections/show"; // limit = 1000
    private final static String RATE_CONTACTS_ADDRESSBOOK = "/contacts/addressbook"; // limit = 300
    private final static String RATE_CONTACTS_DELETE_STATUS = "/contacts/delete/status"; // limit = 300
    private final static String RATE_CONTACTS_UPLOADED_BY = "/contacts/uploaded_by"; // limit = 300
    private final static String RATE_CONTACTS_USERS = "/contacts/users"; // limit = 300
    private final static String RATE_CONTACTS_USERS_AND_UPLOADED_BY = "/contacts/users_and_uploaded_by"; // limit = 300
    private final static String RATE_DEVICE_TOKEN = "/device/token"; // limit = 15
    private final static String RATE_DIRECT_MESSAGES = "/direct_messages"; // limit = 15
    private final static String RATE_DIRECT_MESSAGES_SENT = "/direct_messages/sent"; // limit = 15
    private final static String RATE_DIRECT_MESSAGES_SENT_AND_RECEIVED = "/direct_messages/sent_and_received"; // limit = 15
    private final static String RATE_DIRECT_MESSAGES_SHOW = "/direct_messages/show"; // limit = 15
    private final static String RATE_FAVORITES_LIST = "/favorites/list"; // limit = 15
    private final static String RATE_FOLLOWERS_IDS = "/followers/ids"; // limit = 15
    private final static String RATE_FOLLOWERS_LIST = "/followers/list"; // limit = 15
    private final static String RATE_FRIENDS_FOLLOWING_IDS = "/friends/following/ids"; // limit = 15
    private final static String RATE_FRIENDS_FOLLOWING_LIST = "/friends/following/list"; // limit = 15
    private final static String RATE_FRIENDS_IDS = "/friends/ids"; // limit = 15
    private final static String RATE_FRIENDS_LIST = "/friends/list"; // limit = 15
    private final static String RATE_FRIENDSHIPS_INCOMING = "/friendships/incoming"; // limit = 15
    private final static String RATE_FRIENDSHIPS_LOOKUP = "/friendships/lookup"; // limit = 15
    private final static String RATE_FRIENDSHIPS_NO_RETWEETS_IDS = "/friendships/no_retweets/ids"; // limit = 15
    private final static String RATE_FRIENDSHIPS_OUTGOING = "/friendships/outgoing"; // limit = 15
    private final static String RATE_FRIENDSHIPS_SHOW = "/friendships/show"; // limit = 180
    private final static String RATE_GEO_ID_PLACE_ID = "/geo/id/:place_id"; // limit = 15
    private final static String RATE_GEO_REVERSE_GEOCODE = "/geo/reverse_geocode"; // limit = 15
    private final static String RATE_GEO_SEARCH = "/geo/search"; // limit = 15
    private final static String RATE_GEO_SIMILAR_PLACES = "/geo/similar_places"; // limit = 15
    private final static String RATE_HELP_CONFIGURATION = "/help/configuration"; // limit = 15
    private final static String RATE_HELP_LANGUAGES = "/help/languages"; // limit = 15
    private final static String RATE_HELP_PRIVACY = "/help/privacy"; // limit = 15
    private final static String RATE_HELP_SETTINGS = "/help/settings"; // limit = 15
    private final static String RATE_HELP_TOS = "/help/tos"; // limit = 15
    private final static String RATE_LISTS_LIST = "/lists/list"; // limit = 15
    private final static String RATE_LISTS_MEMBERS = "/lists/members"; // limit = 180
    private final static String RATE_LISTS_MEMBERS_SHOW = "/lists/members/show"; // limit = 15
    private final static String RATE_LISTS_MEMBERSHIPS = "/lists/memberships"; // limit = 15
    private final static String RATE_LISTS_OWNERSHIPS = "/lists/ownerships"; // limit = 15
    private final static String RATE_LISTS_SHOW = "/lists/show"; // limit = 15
    private final static String RATE_LISTS_STATUSES = "/lists/statuses"; // limit = 180
    private final static String RATE_LISTS_SUBSCRIBERS = "/lists/subscribers"; // limit = 180
    private final static String RATE_LISTS_SUBSCRIBERS_SHOW = "/lists/subscribers/show"; // limit = 15
    private final static String RATE_LISTS_SUBSCRIPTIONS = "/lists/subscriptions"; // limit = 15
    private final static String RATE_MUTES_USERS_IDS = "/mutes/users/ids"; // limit = 15
    private final static String RATE_MUTES_USERS_LIST = "/mutes/users/list"; // limit = 15
    private final static String RATE_SAVED_SEARCHES_DESTROY_ID = "/saved_searches/destroy/:id"; // limit = 15
    private final static String RATE_SAVED_SEARCHES_LIST = "/saved_searches/list"; // limit = 15
    private final static String RATE_SAVED_SEARCHES_SHOW_ID = "/saved_searches/show/:id"; // limit = 15
    private final static String RATE_SEARCH_TWEETS = "/search/tweets"; // limit = 180
    private final static String RATE_STATUSES_FRIENDS = "/statuses/friends"; // limit = 15
    private final static String RATE_STATUSES_HOME_TIMELINE = "/statuses/home_timeline"; // limit = 15
    private final static String RATE_STATUSES_LOOKUP = "/statuses/lookup"; // limit = 180
    private final static String RATE_STATUSES_MENTIONS_TIMELINE = "/statuses/mentions_timeline"; // limit = 15
    private final static String RATE_STATUSES_OEMBED = "/statuses/oembed"; // limit = 180
    private final static String RATE_STATUSES_RETWEETERS_IDS = "/statuses/retweeters/ids"; // limit = 15
    private final static String RATE_STATUSES_RETWEETS_ID = "/statuses/retweets/:id"; // limit = 60
    private final static String RATE_STATUSES_RETWEETS_OF_ME = "/statuses/retweets_of_me"; // limit = 15
    private final static String RATE_STATUSES_SHOW_ID = "/statuses/show/:id"; // limit = 180
    private final static String RATE_STATUSES_USER_TIMELINE = "/statuses/user_timeline"; // limit = 180
    private final static String RATE_TRENDS_AVAILABLE = "/trends/available"; // limit = 15
    private final static String RATE_TRENDS_CLOSEST = "/trends/closest"; // limit = 15
    private final static String RATE_TRENDS_PLACE = "/trends/place"; // limit = 15
    private final static String RATE_USERS_DERIVED_INFO = "/users/derived_info"; // limit = 15
    private final static String RATE_USERS_LOOKUP = "/users/lookup"; // limit = 180
    private final static String RATE_USERS_PROFILE_BANNER = "/users/profile_banner"; // limit = 180
    private final static String RATE_USERS_REPORT_SPAM = "/users/report_spam"; // limit = 15
    private final static String RATE_USERS_SEARCH = "/users/search"; // limit = 180
    private final static String RATE_USERS_SHOW_ID = "/users/show/:id"; // limit = 180
    private final static String RATE_USERS_SUGGESTIONS = "/users/suggestions"; // limit = 15
    private final static String RATE_USERS_SUGGESTIONS_SLUG = "/users/suggestions/:slug"; // limit = 15
    private final static String RATE_USERS_SUGGESTIONS_SLUG_MEMBERS = "/users/suggestions/:slug/members"; // limit = 15

    private static TwitterFactory appFactory = null;
    private static Map<String, TwitterFactory> userFactory = new HashMap<>();

    public static TwitterFactory getAppTwitterFactory() {
        String twitterAccessToken = DAO.getConfig("twitterAccessToken", "");
        String twitterAccessTokenSecret = DAO.getConfig("twitterAccessTokenSecret", "");
        if (twitterAccessToken.length() == 0 || twitterAccessTokenSecret.length() == 0)
            return null;
        if (appFactory == null)
            appFactory = getUserTwitterFactory(twitterAccessToken, twitterAccessTokenSecret);
        return appFactory;
    }

    public static TwitterFactory getUserTwitterFactory(String screen_name) {
        TwitterFactory uf = userFactory.get(screen_name);
        if (uf != null)
            return uf;
        AccountEntry accountEntry = DAO.searchLocalAccount(screen_name);
        if (accountEntry == null)
            return null;
        uf = getUserTwitterFactory(accountEntry.getOauthToken(), accountEntry.getOauthTokenSecret());
        if (uf != null)
            userFactory.put(screen_name, uf);
        return uf;
    }

    private static TwitterFactory getUserTwitterFactory(String accessToken, String accessTokenSecret) {
        if (accessToken == null || accessToken.length() == 0 || accessTokenSecret == null
                || accessTokenSecret.length() == 0)
            return null;
        ConfigurationBuilder cb = new ConfigurationBuilder();
        cb.setOAuthConsumerKey(DAO.getConfig("client.twitterConsumerKey", ""))
                .setOAuthConsumerSecret(DAO.getConfig("client.twitterConsumerSecret", ""))
                .setOAuthAccessToken(accessToken).setOAuthAccessTokenSecret(accessTokenSecret);
        cb.setJSONStoreEnabled(true);
        return new TwitterFactory(cb.build());
    }

    public static RateLimitStatus getRateLimitStatus(final String rate_type) throws TwitterException {
        return getAppTwitterFactory().getInstance().getRateLimitStatus().get(rate_type);
    }

    private static final int getUserLimit = 180;
    private static int getUserRemaining = getUserLimit;
    private static long getUserResetTime = 0;

    public static int getUserRemaining() {
        return System.currentTimeMillis() > getUserResetTime ? getUserLimit : getUserRemaining;
    }

    public static JSONObject getUser(String screen_name, boolean forceReload) throws TwitterException, IOException {
        if (!forceReload) {
            JsonFactory mapcapsule = DAO.user_dump.get("screen_name", screen_name);
            if (mapcapsule == null)
                mapcapsule = DAO.user_dump.get("id_str", screen_name);
            if (mapcapsule != null) {
                JSONObject json = mapcapsule.getJSON();
                if (json.length() > 0) {
                    // check if the entry is maybe outdated, i.e. if it is empty or too old
                    try {
                        Date d = DAO.user_dump.parseDate(json);
                        if (d.getTime() + DateParser.DAY_MILLIS > System.currentTimeMillis())
                            return json;
                    } catch (ParseException e) {
                        return json;
                    }
                }
            }
        }
        TwitterFactory tf = getUserTwitterFactory(screen_name);
        if (tf == null)
            tf = getAppTwitterFactory();
        if (tf == null)
            return new JSONObject();
        Twitter twitter = tf.getInstance();
        User user = twitter.showUser(screen_name);
        RateLimitStatus rateLimitStatus = user.getRateLimitStatus();
        getUserResetTime = System.currentTimeMillis() + rateLimitStatus.getSecondsUntilReset() * 1000;
        getUserRemaining = rateLimitStatus.getRemaining();
        JSONObject json = user2json(user);
        enrichLocation(json);
        DAO.user_dump.putUnique(json);
        return json;
    }

    public static JSONObject user2json(User user) throws IOException {
        String jsonstring = TwitterObjectFactory.getRawJSON(user);
        JSONObject json = new JSONObject(jsonstring);
        json.put("retrieval_date", AbstractObjectEntry.utcFormatter.print(System.currentTimeMillis()));
        Object status = json.remove("status"); // we don't need to store the latest status update in the user dump
        // TODO: store the latest status in our message database
        return json;
    }

    /**
     * enrich the user data with geocoding information
     * @param map the user json
     */
    public static void enrichLocation(JSONObject map) {

        // if a location is given, try to reverse geocode to get country name, country code and coordinates
        String location = map.has("location") ? (String) map.get("location") : null;

        // in case that no location is given, we try to hack that information out of the context
        if (location == null || location.length() == 0) {
            // sometimes the time zone contains city names! Try that
            String time_zone = map.has("time_zone") && map.get("time_zone") != JSONObject.NULL
                    ? (String) map.get("time_zone")
                    : null;
            if (time_zone != null && time_zone.length() > 0) {
                GeoMark loc = DAO.geoNames.analyse(time_zone, null, 5, "");
                // check if the time zone was actually a location name
                if (loc != null && loc.getNames().contains(time_zone)) {
                    // success! It's just a guess, however...
                    location = time_zone;
                    map.put("location", location);
                    //DAO.log("enrichLocation: TRANSLATED time_zone to location '" + location + "'");
                }
            }
        }

        // if we finally have a location, then compute country name and geo-coordinates
        if (location != null && location.length() > 0) {
            String location_country = map.has("location_country") ? (String) map.get("location_country") : null;
            String location_country_code = map.has("location_country_code")
                    ? (String) map.get("location_country_code")
                    : null;
            Object location_point = map.has("location_point") ? map.get("location_point") : null;
            Object location_mark = map.has("location") ? map.get("location") : null;
            // maybe we already computed these values before, but they may be incomplete. If they are not complete, we repeat the geocoding
            if (location_country == null || location_country.length() == 0 || location_country_code == null
                    || location_country_code.length() == 0 || location_point == null || location_mark == null) {
                // get a salt 
                String created_at = map.has("created_at") ? (String) map.get("created_at") : null;
                int salt = created_at == null ? map.hashCode() : created_at.hashCode();
                // reverse geocode
                GeoMark loc = DAO.geoNames.analyse(location, null, 5, Integer.toString(salt));
                if (loc != null) {
                    String countryCode = loc.getISO3166cc();
                    if (countryCode != null && countryCode.length() > 0) {
                        String countryName = DAO.geoNames.getCountryName(countryCode);
                        map.put("location_country", countryName);
                        map.put("location_country_code", countryCode);
                    }
                    map.put("location_point", new double[] { loc.lon(), loc.lat() }); //[longitude, latitude]
                    map.put("location_mark", new double[] { loc.mlon(), loc.mlat() }); //[longitude, latitude]
                    //DAO.log("enrichLocation: FOUND   location '" + location + "'");
                } else {
                    //DAO.log("enrichLocation: UNKNOWN location '" + location + "'");
                }
            }
        }

    }

    /**
     * beautify given location information. This should only be called before an export is done, not for storage
     * @param map
     */
    public static void correctLocation(JSONObject map) {
        // if a location is given, try to reverse geocode to get country name, country code and coordinates
        String location = map.has("location") ? (String) map.get("location") : null;

        // if we finally have a location, then compute country name and geo-coordinates
        if (location != null && location.length() > 0) {
            String location_country = map.has("location_country") ? (String) map.get("location_country") : null;

            // maybe we already computed these values before, but they may be incomplete. If they are not complete, we repeat the geocoding
            if (location_country != null && location_country.length() > 0) {
                // check if the location name was made in a "City-Name, Country-Name" schema
                if (location.endsWith(", " + location_country)) {
                    // remove the country name from the location name
                    location = location.substring(0, location.length() - location_country.length() - 2);
                    map.put("location", location);
                    //DAO.log("correctLocation: CORRECTED '" + location + ", " + location_country + "'");
                }
            }
        }
    }

    public static JSONObject getNetwork(String screen_name, int maxFollowers, int maxFollowing)
            throws IOException, TwitterException {
        JSONObject map = new JSONObject(true);
        // we clone the maps because we modify it
        map.putAll(getNetworkerNames(screen_name, maxFollowers, Networker.FOLLOWERS));
        map.putAll(getNetworkerNames(screen_name, maxFollowing, Networker.FOLLOWING));
        map.remove("screen_name");

        //int with_before = 0, with_after = 0;

        for (String setname : new String[] { "followers", "unfollowers", "following", "unfollowing" }) {
            JSONArray users = new JSONArray();
            JSONObject names = map.has(setname + "_names") ? (JSONObject) map.remove(setname + "_names") : null;
            if (names != null) {
                for (String sn : names.keySet()) {
                    JsonFactory user = DAO.user_dump.get("screen_name", sn);
                    if (user != null) {
                        JSONObject usermap = user.getJSON();
                        //if (usermap.get("location") != null && ((String) usermap.get("location")).length() > 0) with_before++;
                        enrichLocation(usermap);
                        correctLocation(usermap);
                        //if (usermap.get("location") != null && ((String) usermap.get("location")).length() > 0) with_after++;
                        users.put(usermap);
                    }
                }
            }
            //if (users.size() > 0) DAO.log("enrichLocation result: set = " + setname + ", users = " + users.size() + ", with location before = " + with_before + ", with location after = " + with_after + ", success = " + (100 * (with_after - with_before) / users.size()) + "%");
            map.put(setname + "_count", users.length());
            map.put(setname, users);
        }

        return map;
    }

    private static enum Networker {
        FOLLOWERS, FOLLOWING;
    }

    private static final int getFollowerIdLimit = 180, getFollowingIdLimit = 180;
    private static int getFollowerIdRemaining = getFollowerIdLimit, getFollowingIdRemaining = getFollowingIdLimit;
    private static long getFollowerIdResetTime = 0, getFollowingIdResetTime = 0;

    public static int getFollowerIdRemaining() {
        return System.currentTimeMillis() > getFollowerIdResetTime ? getFollowerIdLimit : getFollowerIdRemaining;
    }

    public static int getFollowingIdRemaining() {
        return System.currentTimeMillis() > getFollowingIdResetTime ? getFollowingIdLimit : getFollowingIdRemaining;
    }

    public static JSONObject getFollowersNames(final String screen_name, final int max_count)
            throws IOException, TwitterException {
        return getNetworkerNames(screen_name, max_count, Networker.FOLLOWERS);
    }

    public static JSONObject getFollowingNames(final String screen_name, final int max_count)
            throws IOException, TwitterException {
        return getNetworkerNames(screen_name, max_count, Networker.FOLLOWING);
    }

    public static JSONObject getNetworkerNames(final String screen_name, final int max_count,
            final Networker networkRelation) throws IOException, TwitterException {
        if (max_count == 0)
            return new JSONObject();
        boolean complete = true;
        Set<Number> networkingIDs = new LinkedHashSet<>();
        Set<Number> unnetworkingIDs = new LinkedHashSet<>();
        JsonFactory mapcapsule = (networkRelation == Networker.FOLLOWERS ? DAO.followers_dump : DAO.following_dump)
                .get("screen_name", screen_name);
        if (mapcapsule == null) {
            JsonDataset ds = networkRelation == Networker.FOLLOWERS ? DAO.followers_dump : DAO.following_dump;
            mapcapsule = ds.get("screen_name", screen_name);
        }

        if (mapcapsule != null) {
            JSONObject json = mapcapsule.getJSON();

            // check date and completeness
            complete = json.has("complete") ? (Boolean) json.get("complete") : Boolean.FALSE;
            String retrieval_date_string = json.has("retrieval_date") ? (String) json.get("retrieval_date") : null;
            DateTime retrieval_date = retrieval_date_string == null ? null
                    : AbstractObjectEntry.utcFormatter.parseDateTime(retrieval_date_string);
            if (complete && System.currentTimeMillis() - retrieval_date.getMillis() < DateParser.DAY_MILLIS)
                return json;

            // load networking ids for incomplete retrievals (untested)
            String nr = networkRelation == Networker.FOLLOWERS ? "follower" : "following";
            if (json.has(nr)) {
                JSONArray fro = json.getJSONArray(nr);
                for (Object f : fro)
                    networkingIDs.add((Number) f);
            }
        }
        TwitterFactory tf = getUserTwitterFactory(screen_name);
        if (tf == null)
            tf = getAppTwitterFactory();
        if (tf == null)
            return new JSONObject();
        Twitter twitter = tf.getInstance();
        long cursor = -1;
        collect: while (cursor != 0) {
            try {
                IDs ids = networkRelation == Networker.FOLLOWERS ? twitter.getFollowersIDs(screen_name, cursor)
                        : twitter.getFriendsIDs(screen_name, cursor);
                RateLimitStatus rateStatus = ids.getRateLimitStatus();
                if (networkRelation == Networker.FOLLOWERS) {
                    getFollowerIdRemaining = rateStatus.getRemaining();
                    getFollowerIdResetTime = System.currentTimeMillis() + rateStatus.getSecondsUntilReset() * 1000;
                } else {
                    getFollowingIdRemaining = rateStatus.getRemaining();
                    getFollowingIdResetTime = System.currentTimeMillis() + rateStatus.getSecondsUntilReset() * 1000;
                }
                //System.out.println("got: " + ids.getIDs().length + " ids");
                //System.out.println("Rate Status: " + rateStatus.toString() + "; time=" + System.currentTimeMillis());
                boolean dd = false;
                for (long id : ids.getIDs()) {
                    if (networkingIDs.contains(id))
                        dd = true; // don't break loop here
                    networkingIDs.add(id);
                }
                if (dd)
                    break collect; // this is complete!
                if (rateStatus.getRemaining() == 0) {
                    complete = false;
                    break collect;
                }
                if (networkingIDs.size() >= Math.min(10000, max_count >= 0 ? max_count : 10000)) {
                    complete = false;
                    break collect;
                }
                cursor = ids.getNextCursor();
            } catch (TwitterException e) {
                complete = false;
                break collect;
            }
        }
        // create result
        JSONObject json = new JSONObject(true);
        json.put("screen_name", screen_name);
        json.put("retrieval_date", AbstractObjectEntry.utcFormatter.print(System.currentTimeMillis()));
        json.put("complete", complete);
        Map<String, Number> networking = getScreenName(networkingIDs, max_count, true);
        Map<String, Number> unnetworking = getScreenName(unnetworkingIDs, max_count, true);
        if (networkRelation == Networker.FOLLOWERS) {
            json.put("followers_count", networking.size());
            json.put("unfollowers_count", unnetworking.size());
            json.put("followers_names", networking);
            json.put("unfollowers_names", unnetworking);
            if (complete)
                DAO.followers_dump.putUnique(json); // currently we write only complete data sets. In the future the update of datasets shall be supported
        } else {
            json.put("following_count", networking.size());
            json.put("unfollowing_count", unnetworking.size());
            json.put("following_names", networking);
            json.put("unfollowing_names", unnetworking);
            if (complete)
                DAO.following_dump.putUnique(json);
        }
        return json;
    }

    /**
     * search for twitter user names by a given set of user id's
     * @param id_strs
     * @param lookupLocalUsersByUserId if this is true and successful, the resulting names may contain users without user info in the user dump
     * @return
     * @throws IOException
     * @throws TwitterException 
     */
    public static Map<String, Number> getScreenName(Set<Number> id_strs, final int maxFollowers,
            boolean lookupLocalUsersByUserId) throws IOException, TwitterException {
        // we have several sources to get this mapping:
        // - 1st / fastest: mapping from DAO.twitter_user_dump
        // - 2nd / fast   : mapping from DAO.searchLocalUserByUserId(user_id)
        // - 3rd / slow   : from twitter API with twitter.lookupUsers(String[] user_id)
        // first we check all fast solutions until trying the twitter api
        Map<String, Number> r = new HashMap<>();
        Set<Number> id4api = new HashSet<>();
        for (Number id_str : id_strs) {
            if (r.size() >= maxFollowers)
                break;
            JsonFactory mapcapsule = DAO.user_dump.get("id_str", id_str.toString());
            if (mapcapsule != null) {
                JSONObject map = mapcapsule.getJSON();
                String screen_name = map.has("screen_name") ? (String) map.get("screen_name") : null;
                if (screen_name != null) {
                    r.put(screen_name, id_str);
                    continue;
                }
            }
            if (lookupLocalUsersByUserId) {
                UserEntry ue = DAO.searchLocalUserByUserId(id_str.toString());
                if (ue != null) {
                    String screen_name = ue.getScreenName();
                    if (screen_name != null) {
                        r.put(screen_name, id_str);
                        continue;
                    }
                }
            }
            id4api.add(id_str);
        }

        while (id4api.size() > 100 && id4api.size() + r.size() > maxFollowers)
            id4api.remove(id4api.iterator().next());

        // resolve the remaining user_ids from the twitter api
        if (r.size() < maxFollowers && id4api.size() > 0) {
            TwitterFactory tf = getAppTwitterFactory();
            if (tf == null)
                return new HashMap<>();
            Twitter twitter = tf.getInstance();
            collect: while (id4api.size() > 0) {
                // construct a query term with at most 100 id's
                int chunksize = Math.min(100, id4api.size());
                long[] u = new long[chunksize];
                Iterator<Number> ni = id4api.iterator();
                for (int i = 0; i < chunksize; i++) {
                    u[i] = ni.next().longValue();
                }
                try {
                    ResponseList<User> users = twitter.lookupUsers(u);
                    for (User usr : users) {
                        JSONObject map = user2json(usr);
                        enrichLocation(map);
                        DAO.user_dump.putUnique(map);
                        r.put(usr.getScreenName(), usr.getId());
                        id4api.remove(usr.getId());
                    }
                } catch (TwitterException e) {
                    if (r.size() == 0)
                        throw e;
                    break collect;
                }
            }
        }
        return r;
    }

    public static void main(String[] args) {
        try {
            Path data = FileSystems.getDefault().getPath("data");
            DAO.init(LoklakServer.readConfig(data), data);
            try {
                System.out.println(getRateLimitStatus(RATE_FOLLOWERS_IDS));
            } catch (TwitterException e) {
                e.printStackTrace();
            }
            try {
                System.out.println(getFollowersNames("loklak_app", 10000));
            } catch (IOException | TwitterException e) {
                e.printStackTrace();
            }
            try {
                System.out.println(getFollowingNames("loklak_app", 10000));
            } catch (IOException | TwitterException e) {
                e.printStackTrace();
            }
            DAO.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        System.exit(0);
    }

}