package com.piusvelte.sonet.social; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.piusvelte.sonet.BuildConfig; import com.piusvelte.sonet.R; import com.piusvelte.sonet.Sonet; import com.piusvelte.sonet.SonetCrypto; import com.piusvelte.sonet.SonetHttpClient; import com.piusvelte.sonet.provider.Entity; import com.piusvelte.sonet.provider.Notifications; import com.piusvelte.sonet.provider.Statuses; import com.squareup.okhttp.FormEncodingBuilder; import com.squareup.okhttp.Request; import org.apache.http.client.methods.HttpPost; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import static com.piusvelte.sonet.Sonet.Screated_at; import static com.piusvelte.sonet.Sonet.Sfull_name; import static com.piusvelte.sonet.Sonet.Sid; import static com.piusvelte.sonet.Sonet.Sin_reply_to_status_id; import static com.piusvelte.sonet.Sonet.Sname; import static com.piusvelte.sonet.Sonet.Splaces; import static com.piusvelte.sonet.Sonet.Sprofile_image_url; import static com.piusvelte.sonet.Sonet.Sresult; import static com.piusvelte.sonet.Sonet.Sscreen_name; import static com.piusvelte.sonet.Sonet.Sstatus; import static com.piusvelte.sonet.Sonet.Stext; import static com.piusvelte.sonet.Sonet.Suser; /** * Created by bemmanuel on 2/15/15. */ public class Twitter extends Client { private static final String TWITTER_BASE_URL = "https://api.twitter.com/"; private static final String TWITTER_URL_REQUEST = "%soauth/request_token"; private static final String TWITTER_URL_AUTHORIZE = "%soauth/authorize"; private static final String TWITTER_URL_ACCESS = "%soauth/access_token"; private static final String TWITTER_URL_FEED = "%s1.1/statuses/home_timeline.json?count=%s"; private static final String TWITTER_RETWEET = "%s1.1/statuses/retweets/%s.json"; private static final String TWITTER_USER = "%s1.1/users/show.json?user_id=%s"; private static final String TWITTER_UPDATE = "%s1.1/statuses/update.json"; private static final String TWITTER_SEARCH = "%s1.1/geo/search.json?lat=%s&long=%s"; private static final String TWITTER_DATE_FORMAT = "EEE MMM dd HH:mm:ss Z yyyy"; private static final String TWITTER_MENTIONS = "%s1.1/statuses/mentions_timeline.json%s"; private static final String TWITTER_SINCE_ID = "?since_id=%s"; private static final String TWITTER_VERIFY_CREDENTIALS = "%s1.1/account/verify_credentials.json"; private static final String TWITTER_PROFILE = "http://twitter.com/%s"; public Twitter(Context context, String token, String secret, String accountEsid, int network) { super(context, token, secret, accountEsid, network); } @Nullable @Override public String getProfileUrl(@NonNull String esid) { Request request = getOAuth10Helper().getBuilder() .url(String.format(getUserUrl(), getBaseUrl(), esid)) .build(); String response = SonetHttpClient.getResponse(request); if (!TextUtils.isEmpty(response)) { try { JSONObject user = new JSONObject(response); return String.format(getProfileUrl(), user.getString("screen_name")); } catch (JSONException e) { if (BuildConfig.DEBUG) { Log.e(mTag, "Error parsing: " + response, e); } } } return null; } @Nullable @Override public String getProfilePhotoUrl() { return getProfilePhotoUrl(mAccountEsid); } @Nullable @Override public String getProfilePhotoUrl(String esid) { Request request = getOAuth10Helper().getBuilder() .url(String.format(getUserUrl(), getBaseUrl(), esid)) .build(); String response = SonetHttpClient.getResponse(request); if (!TextUtils.isEmpty(response)) { try { JSONObject user = new JSONObject(response); return user.getString(Sprofile_image_url); } catch (JSONException e) { if (BuildConfig.DEBUG) { Log.e(mTag, "Error parsing: " + response, e); } } } return null; } @Nullable @Override public Uri getCallback() { return Uri.parse("sonet://twitter"); } @Override String getRequestUrl() { return String.format(getRequestUrlFormat(), getBaseUrl()); } @Override String getAccessUrl() { return String.format(getAccessUrlFormat(), getBaseUrl()); } @Override String getAuthorizeUrl() { return String.format(getAuthorizeUrlFormat(), getBaseUrl()); } @Override public String getCallbackUrl() { return getCallback().toString(); } @Override public MemberAuthentication getMemberAuthentication(@NonNull String authenticatedUrl) { if (getOAuth10Helper().getAccessToken(SonetHttpClient.getOkHttpClientInstance(), authenticatedUrl)) { Request request = getOAuth10Helper().getBuilder() .url(getVerifyCredentialsUrl()) .build(); String httpResponse = SonetHttpClient.getResponse(request); if (!TextUtils.isEmpty(httpResponse)) { try { JSONObject jobj = new JSONObject(httpResponse); MemberAuthentication memberAuthentication = new MemberAuthentication(); memberAuthentication.username = jobj.getString(Sscreen_name); memberAuthentication.token = getOAuth10Helper().getToken(); memberAuthentication.secret = getOAuth10Helper().getSecret(); memberAuthentication.expiry = 0; memberAuthentication.network = mNetwork; memberAuthentication.id = jobj.getString(Sid); return memberAuthentication; } catch (JSONException e) { if (BuildConfig.DEBUG) Log.e(mTag, e.getMessage()); } } } return null; } String getBaseUrl() { return TWITTER_BASE_URL; } String getFeedUrl() { return TWITTER_URL_FEED; } String getMentionsUrl() { return TWITTER_MENTIONS; } String getUpdateUrl() { return TWITTER_UPDATE; } String getSearchUrl() { return TWITTER_SEARCH; } String getUserUrl() { return TWITTER_USER; } String getRetweetUrl() { return TWITTER_RETWEET; } String getRequestUrlFormat() { return TWITTER_URL_REQUEST; } String getAccessUrlFormat() { return TWITTER_URL_ACCESS; } String getAuthorizeUrlFormat() { return TWITTER_URL_AUTHORIZE; } String getVerifyCredentialsUrl() { return String.format(TWITTER_VERIFY_CREDENTIALS, getBaseUrl()); } String getProfileUrl() { return TWITTER_PROFILE; } @Override void formatLink(Matcher matcher, StringBuffer stringBuffer, String link) { // NO-OP } @Nullable @Override public Set<String> getNotificationStatusIds(long account, String[] notificationMessage) { return null; } @Nullable @Override public String getFeedResponse(int status_count) { Request request = getOAuth10Helper().getBuilder() .url(String.format(getFeedUrl(), getBaseUrl(), status_count)) .build(); return SonetHttpClient.getResponse(request); } @Nullable @Override public JSONArray parseFeed(@NonNull String response) throws JSONException { return new JSONArray(response); } @Nullable @Override public void addFeedItem(@NonNull JSONObject item, boolean display_profile, boolean time24hr, int appWidgetId, long account, Set<String> notificationSids, String[] notificationMessage, boolean doNotify) throws JSONException { JSONObject user = item.getJSONObject(Suser); addStatusItem(parseDate(item.getString(Screated_at), TWITTER_DATE_FORMAT), user.getString(Sname), display_profile ? user.getString(Sprofile_image_url) : null, item.getString(Stext), time24hr, appWidgetId, account, item.getString(Sid), user.getString(Sid)); } @Nullable @Override public void getNotificationMessage(long account, String[] notificationMessage) { getNotifications(account, notificationMessage); } @Override public void getNotifications(long account, String[] notificationMessage) { ArrayList<String> notificationSids = new ArrayList<>(); String sid; String friend; Cursor currentNotifications = getContentResolver() .query(Notifications.getContentUri(mContext), new String[] { Notifications.SID }, Notifications.ACCOUNT + "=?", new String[] { Long.toString(account) }, null); // loop over notifications if (currentNotifications.moveToFirst()) { // store sids, to avoid duplicates when requesting the latest feed sid = SonetCrypto.getInstance(mContext).Decrypt(currentNotifications.getString(0)); if (!notificationSids.contains(sid)) { notificationSids.add(sid); } } currentNotifications.close(); // limit to oldest status String last_sid = null; Cursor last_status = getContentResolver().query(Statuses.getContentUri(mContext), new String[] { Statuses.SID }, Statuses.ACCOUNT + "=?", new String[] { Long.toString(account) }, Statuses.CREATED + " ASC LIMIT 1"); if (last_status.moveToFirst()) { last_sid = SonetCrypto.getInstance(mContext).Decrypt(last_status.getString(0)); } last_status.close(); // get all mentions since the oldest status for this account Request request = getOAuth10Helper().getBuilder() .url(String.format(getMentionsUrl(), getBaseUrl(), last_sid != null ? String.format(TWITTER_SINCE_ID, last_sid) : "")) .build(); String response = SonetHttpClient.getResponse(request); if (!TextUtils.isEmpty(response)) { try { JSONObject statusObj; JSONObject friendObj; JSONArray statusesArray = new JSONArray(response); for (int i = 0, i2 = statusesArray.length(); i < i2; i++) { statusObj = statusesArray.getJSONObject(i); friendObj = statusObj.getJSONObject(Suser); if (!friendObj.getString(Sid).equals(mAccountEsid) && !notificationSids.contains(statusObj.getString(Sid))) { friend = friendObj.getString(Sname); addNotification(statusObj.getString(Sid), friendObj.getString(Sid), friend, statusObj.getString(Stext), parseDate(statusObj.getString(Screated_at), TWITTER_DATE_FORMAT), account, friend + " mentioned you on Twitter"); updateNotificationMessage(notificationMessage, friend + " mentioned you on Twitter"); } } } catch (JSONException e) { if (BuildConfig.DEBUG) Log.e(mTag, "error parsing response", e); } } } private int getNext140CharactersIndex(@NonNull String message, int startIndex) { // check if the message needs to be trimmed int messageLength = message.length(); if (messageLength - startIndex > 140) { // this is the max stopIndex int stopIndex = Math.min(messageLength, startIndex + 140); // find the last space to break on, defaulting to cutting at startIndex + 140 for (int nextCharIndex = stopIndex - 1; nextCharIndex > startIndex; nextCharIndex--) { if (" ".equals(message.substring(nextCharIndex, nextCharIndex + 1))) { stopIndex = nextCharIndex + 1; break; } } return stopIndex; } return messageLength; } @Override public boolean createPost(String message, String placeId, String latitude, String longitude, String photoPath, String[] tags) { boolean result = false; int startTweetIndex = 0; // limit tweets to 140, breaking up the message if necessary do { int endTweetIndex = getNext140CharactersIndex(message, startTweetIndex); String tweet = message.substring(startTweetIndex, endTweetIndex); FormEncodingBuilder builder = new FormEncodingBuilder() .add("http.protocol.expect-continue", Boolean.FALSE.toString()) .add(Sstatus, tweet); if (placeId != null) { builder.add("place_id", placeId) .add("lat", latitude) .add("long", longitude); } Request request = getOAuth10Helper().getBuilder() .url(String.format(getUpdateUrl(), getBaseUrl())) .post(builder.build()) .build(); result = SonetHttpClient.request(request); if (!result) { break; } // advance the start index for the next tweet startTweetIndex = endTweetIndex; } while (startTweetIndex < message.length()); return result; } @Override public boolean isLikeable(String statusId) { // retweetable return true; } @Override public boolean isLiked(String statusId, String accountId) { return false; } @Override public boolean likeStatus(String statusId, String accountId, boolean doLike) { if (doLike) { // retweet Request request = getOAuth10Helper().getBuilder() .url(String.format(getRetweetUrl(), getBaseUrl(), statusId)) .post(new FormEncodingBuilder() .add("http.protocol.expect-continue", Boolean.FALSE.toString()) .build()) .build(); return SonetHttpClient.request(request); } return false; } @Override public String getLikeText(boolean isLiked) { return getString(R.string.retweet); } @Override public boolean isCommentable(String statusId) { return true; } @Override public String getCommentPretext(String accountId) { return "@" + getScreenName(accountId) + " "; } @Override public void onDelete() { } @Override public String getCommentsResponse(String statusId) { Request request = getOAuth10Helper().getBuilder() .url(String.format(getMentionsUrl(), getBaseUrl(), String.format(TWITTER_SINCE_ID, statusId))) .build(); return SonetHttpClient.getResponse(request); } @Override public JSONArray parseComments(String response) throws JSONException { return new JSONArray(response); } @Override public HashMap<String, String> parseComment(String statusId, JSONObject jsonComment, boolean time24hr) throws JSONException { String replyId = null; try { replyId = jsonComment.getString(Sin_reply_to_status_id); } catch (JSONException e) { if (BuildConfig.DEBUG) Log.d(mTag, "exception getting reply id", e); } if (statusId.equals(replyId)) { HashMap<String, String> commentMap = new HashMap<>(); commentMap.put(Statuses.SID, jsonComment.getString(Sid)); commentMap.put(Entity.FRIEND, jsonComment.getJSONObject(Suser).getString(Sname)); commentMap.put(Statuses.MESSAGE, jsonComment.getString(Stext)); commentMap.put(Statuses.CREATEDTEXT, Sonet.getCreatedText(parseDate(jsonComment.getString(Screated_at), TWITTER_DATE_FORMAT), time24hr)); commentMap.put(getString(R.string.like), getLikeText(true)); return commentMap; } return null; } @Override public LinkedHashMap<String, String> getLocations(String latitude, String longitude) { // anonymous requests are rate limited to 150 per hour // authenticated requests are rate limited to 350 per hour, so authenticate this! Request request = getOAuth10Helper().getBuilder() .url(String.format(getSearchUrl(), getBaseUrl(), latitude, longitude)) .build(); String response = SonetHttpClient.getResponse(request); if (response != null) { LinkedHashMap<String, String> locations = new LinkedHashMap<String, String>(); try { JSONArray places = new JSONObject(response).getJSONObject(Sresult).getJSONArray(Splaces); for (int i = 0, i2 = places.length(); i < i2; i++) { JSONObject place = places.getJSONObject(i); locations.put(place.getString(Sid), place.getString(Sfull_name)); } } catch (JSONException e) { if (BuildConfig.DEBUG) Log.e(mTag, e.toString()); } return locations; } return null; } @Override public boolean sendComment(@NonNull String statusId, @NonNull String message) { boolean success; HttpPost httpPost; int startTweetIndex = 0; // limit tweets to 140, breaking up the message if necessary do { int endTweetIndex = getNext140CharactersIndex(message, startTweetIndex); String tweet = message.substring(startTweetIndex, endTweetIndex); FormEncodingBuilder builder = new FormEncodingBuilder() .add("http.protocol.expect-continue", Boolean.FALSE.toString()) .add(Sstatus, tweet) .add(Sin_reply_to_status_id, statusId); Request request = getOAuth10Helper().getBuilder() .url(String.format(getUpdateUrl(), getBaseUrl())) .post(builder.build()) .build(); success = SonetHttpClient.request(request); if (!success) { break; } // advance the start index for the next tweet startTweetIndex = endTweetIndex; } while (startTweetIndex < message.length()); return success; } @Override public List<HashMap<String, String>> getFriends() { return null; } @Nullable private String getScreenName(String accountId) { Request request = getOAuth10Helper().getBuilder() .url(String.format(getUserUrl(), getBaseUrl(), accountId)) .build(); String response = SonetHttpClient.getResponse(request); if (response != null) { try { JSONObject user = new JSONObject(response); return user.getString(Sscreen_name); } catch (JSONException e) { if (BuildConfig.DEBUG) Log.e(mTag, e.toString()); } } return null; } @Override String getApiKey() { return BuildConfig.TWITTER_KEY; } @Override String getApiSecret() { return BuildConfig.TWITTER_SECRET; } }