org.andstatus.app.net.social.ConnectionPumpio.java Source code

Java tutorial

Introduction

Here is the source code for org.andstatus.app.net.social.ConnectionPumpio.java

Source

/*
 * Copyright (C) 2013 yvolk (Yuri Volkov), http://yurivolkov.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.andstatus.app.net.social;

import android.net.Uri;
import android.text.TextUtils;

import org.andstatus.app.context.MyContextHolder;
import org.andstatus.app.data.MyContentType;
import org.andstatus.app.net.http.ConnectionException;
import org.andstatus.app.net.http.ConnectionException.StatusCode;
import org.andstatus.app.net.http.HttpConnection;
import org.andstatus.app.net.http.HttpConnectionData;
import org.andstatus.app.origin.OriginConnectionData;
import org.andstatus.app.util.MyHtml;
import org.andstatus.app.util.MyLog;
import org.andstatus.app.util.TriState;
import org.andstatus.app.util.UrlUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Implementation of pump.io API: <a href="https://github.com/e14n/pump.io/blob/master/API.md">https://github.com/e14n/pump.io/blob/master/API.md</a>  
 * @author yvolk@yurivolkov.com
 */
public class ConnectionPumpio extends Connection {
    private static final String TAG = ConnectionPumpio.class.getSimpleName();

    @Override
    public void enrichConnectionData(OriginConnectionData connectionData) {
        super.enrichConnectionData(connectionData);
        if (!TextUtils.isEmpty(connectionData.getAccountUsername())) {
            connectionData.setOriginUrl(
                    UrlUtils.buildUrl(usernameToHost(connectionData.getAccountUsername()), connectionData.isSsl()));
        }
    }

    @Override
    protected String getApiPath1(ApiRoutineEnum routine) {
        String url;
        switch (routine) {
        case ACCOUNT_VERIFY_CREDENTIALS:
            url = "whoami";
            break;
        case GET_FRIENDS:
        case GET_FRIENDS_IDS:
            url = "user/%nickname%/following";
            break;
        case GET_USER:
            url = "user/%nickname%/profile";
            break;
        case REGISTER_CLIENT:
            url = "client/register";
            break;
        case STATUSES_HOME_TIMELINE:
            url = "user/%nickname%/inbox";
            break;
        case POST_WITH_MEDIA:
            url = "user/%nickname%/uploads";
            break;
        case CREATE_FAVORITE:
        case DESTROY_FAVORITE:
        case FOLLOW_USER:
        case POST_DIRECT_MESSAGE:
        case POST_REBLOG:
        case DESTROY_MESSAGE:
        case POST_MESSAGE:
        case STATUSES_USER_TIMELINE:
            url = "user/%nickname%/feed";
            break;
        default:
            url = "";
            break;
        }
        return prependWithBasicPath(url);
    }

    @Override
    public MbRateLimitStatus rateLimitStatus() throws ConnectionException {
        // TODO Method stub
        return new MbRateLimitStatus();
    }

    @Override
    public MbUser verifyCredentials() throws ConnectionException {
        JSONObject user = http.getRequest(getApiPath(ApiRoutineEnum.ACCOUNT_VERIFY_CREDENTIALS));
        return userFromJson(user);
    }

    private MbUser userFromJson(JSONObject jso) throws ConnectionException {
        if (!PumpioObjectType.PERSON.isMyType(jso)) {
            return MbUser.getEmpty();
        }
        String oid = jso.optString("id");
        MbUser user = MbUser.fromOriginAndUserOid(data.getOriginId(), oid);
        user.actor = MbUser.fromOriginAndUserOid(data.getOriginId(), data.getAccountUserOid());
        user.setUserName(userOidToUsername(oid));
        user.oid = oid;
        user.realName = jso.optString("displayName");
        if (jso.has("image")) {
            JSONObject image = jso.optJSONObject("image");
            if (image != null) {
                user.avatarUrl = image.optString("url");
            }
        }
        user.description = jso.optString("summary");
        user.homepage = jso.optString("url");
        user.setUrl(jso.optString("url"));
        user.updatedDate = dateFromJson(jso, "updated");
        return user;
    }

    private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.GERMANY);

    /**
     * Simple solution based on:
     * http://stackoverflow.com/questions/2201925/converting-iso8601-compliant-string-to-java-util-date
     * @return Unix time. Returns 0 in a case of an error
     */
    @Override
    public long parseDate(String stringDate) {
        long unixDate = 0;
        if (stringDate != null) {
            String datePrepared;
            if (stringDate.lastIndexOf('Z') == stringDate.length() - 1) {
                datePrepared = stringDate.substring(0, stringDate.length() - 1) + "+0000";
            } else {
                datePrepared = stringDate.replaceAll("\\+0([0-9]){1}\\:00", "+0$100");
            }
            try {
                unixDate = dateFormat.parse(datePrepared).getTime();
            } catch (ParseException e) {
                MyLog.e(this, "Failed to parse the date: '" + stringDate + "'", e);
            }
        }
        return unixDate;
    }

    @Override
    public MbMessage destroyFavorite(String messageId) throws ConnectionException {
        return verbWithMessage("unfavorite", messageId);
    }

    @Override
    public MbMessage createFavorite(String messageId) throws ConnectionException {
        return verbWithMessage("favorite", messageId);
    }

    @Override
    public boolean destroyStatus(String messageId) throws ConnectionException {
        MbMessage message = verbWithMessage("delete", messageId);
        return !message.isEmpty();
    }

    private MbMessage verbWithMessage(String verb, String messageId) throws ConnectionException {
        return ActivitySender.fromId(this, messageId).sendMessage(verb);
    }

    @Override
    public List<MbUser> getUsersFollowedBy(String userId) throws ConnectionException {
        int limit = 200;
        ApiRoutineEnum apiRoutine = ApiRoutineEnum.GET_FRIENDS;
        ConnectionAndUrl conu = getConnectionAndUrl(apiRoutine, userId);
        Uri sUri = Uri.parse(conu.url);
        Uri.Builder builder = sUri.buildUpon();
        if (fixedDownloadLimitForApiRoutine(limit, apiRoutine) > 0) {
            builder.appendQueryParameter("count",
                    String.valueOf(fixedDownloadLimitForApiRoutine(limit, apiRoutine)));
        }
        String url = builder.build().toString();
        JSONArray jArr = conu.httpConnection.getRequestAsArray(url);
        List<MbUser> followedUsers = new ArrayList<MbUser>();
        if (jArr != null) {
            for (int index = 0; index < jArr.length(); index++) {
                try {
                    JSONObject jso = jArr.getJSONObject(index);
                    MbUser item = userFromJson(jso);
                    followedUsers.add(item);
                } catch (JSONException e) {
                    throw ConnectionException.loggedJsonException(this, "Parsing list of users", e, null);
                }
            }
        }
        MyLog.d(TAG, "getUsersFollowedBy '" + url + "' " + followedUsers.size() + " users");
        return followedUsers;
    }

    @Override
    public MbMessage getMessage1(String messageId) throws ConnectionException {
        JSONObject message = http.getRequest(messageId);
        return messageFromJson(message);
    }

    @Override
    public MbMessage updateStatus(String messageIn, String inReplyToId, Uri mediaUri) throws ConnectionException {
        String message = toHtmlIfAllowed(messageIn);
        ActivitySender sender = ActivitySender.fromContent(this, message);
        sender.setInReplyTo(inReplyToId);
        sender.setMediaUri(mediaUri);
        return messageFromJson(sender.sendMe("post"));
    }

    protected String toHtmlIfAllowed(String message) {
        return MyContextHolder.get().persistentOrigins().isHtmlContentAllowed(data.getOriginId())
                ? MyHtml.htmLify(message)
                : message;
    }

    String oidToObjectType(String oid) {
        String objectType = "";
        if (oid.contains("/comment/")) {
            objectType = "comment";
        } else if (oid.startsWith("acct:")) {
            objectType = "person";
        } else if (oid.contains("/note/")) {
            objectType = "note";
        } else if (oid.contains("/notice/")) {
            objectType = "note";
        } else if (oid.contains("/person/")) {
            objectType = "person";
        } else if (oid.contains("/user/")) {
            objectType = "person";
        } else {
            String pattern = "/api/";
            int indStart = oid.indexOf(pattern);
            if (indStart >= 0) {
                int indEnd = oid.indexOf("/", indStart + pattern.length());
                if (indEnd > indStart) {
                    objectType = oid.substring(indStart + pattern.length(), indEnd);
                }
            }
        }
        if (TextUtils.isEmpty(objectType)) {
            objectType = "unknown object type: " + oid;
            MyLog.e(this, objectType);
        }
        return objectType;
    }

    ConnectionAndUrl getConnectionAndUrl(ApiRoutineEnum apiRoutine, String userId) throws ConnectionException {
        ConnectionAndUrl conu = new ConnectionAndUrl();
        conu.url = this.getApiPath(apiRoutine);
        if (TextUtils.isEmpty(conu.url)) {
            throw new ConnectionException(StatusCode.UNSUPPORTED_API,
                    "The API is not supported yet: " + apiRoutine);
        }
        if (TextUtils.isEmpty(userId)) {
            throw new IllegalArgumentException(apiRoutine + ": userId is required");
        }
        String username = userOidToUsername(userId);
        String nickname = usernameToNickname(username);
        if (TextUtils.isEmpty(nickname)) {
            throw new IllegalArgumentException(apiRoutine + ": wrong userId=" + userId);
        }
        String host = usernameToHost(username);
        conu.httpConnection = http;
        if (TextUtils.isEmpty(host)) {
            throw new IllegalArgumentException(apiRoutine + ": host is empty for the userId=" + userId);
        } else if (http.data.originUrl == null || host.compareToIgnoreCase(http.data.originUrl.getHost()) != 0) {
            MyLog.v(this, "Requesting data from the host: " + host);
            HttpConnectionData connectionData1 = http.data.clone();
            connectionData1.oauthClientKeys = null;
            connectionData1.originUrl = UrlUtils.buildUrl(host, connectionData1.isSsl);
            conu.httpConnection = http.getNewInstance();
            conu.httpConnection.setConnectionData(connectionData1);
        }
        if (!conu.httpConnection.data.areOAuthClientKeysPresent()) {
            conu.httpConnection.registerClient(getApiPath(ApiRoutineEnum.REGISTER_CLIENT));
            if (!conu.httpConnection.getCredentialsPresent()) {
                throw ConnectionException.fromStatusCodeAndHost(StatusCode.NO_CREDENTIALS_FOR_HOST,
                        "No credentials", conu.httpConnection.data.originUrl);
            }
        }
        conu.url = conu.url.replace("%nickname%", nickname);
        return conu;
    }

    static class ConnectionAndUrl {
        String url;
        HttpConnection httpConnection;
    }

    @Override
    public MbMessage postDirectMessage(String messageIn, String recipientId, Uri mediaUri)
            throws ConnectionException {
        String message = toHtmlIfAllowed(messageIn);
        ActivitySender sender = ActivitySender.fromContent(this, message);
        sender.setRecipient(recipientId);
        sender.setMediaUri(mediaUri);
        return messageFromJson(sender.sendMe("post"));
    }

    @Override
    public MbMessage postReblog(String rebloggedId) throws ConnectionException {
        return verbWithMessage("share", rebloggedId);
    }

    @Override
    public List<MbTimelineItem> getTimeline(ApiRoutineEnum apiRoutine, TimelinePosition sinceId, int limit,
            String userId) throws ConnectionException {
        ConnectionAndUrl conu = getConnectionAndUrl(apiRoutine, userId);
        Uri sUri = Uri.parse(conu.url);
        Uri.Builder builder = sUri.buildUpon();
        if (!sinceId.isEmpty()) {
            // The "since" should point to the "Activity" on the timeline, not to the message
            // Otherwise we will always get "not found"
            builder.appendQueryParameter("since", sinceId.getPosition());
        }
        if (fixedDownloadLimitForApiRoutine(limit, apiRoutine) > 0) {
            builder.appendQueryParameter("count",
                    String.valueOf(fixedDownloadLimitForApiRoutine(limit, apiRoutine)));
        }
        String url = builder.build().toString();
        JSONArray jArr = conu.httpConnection.getRequestAsArray(url);
        List<MbTimelineItem> timeline = new ArrayList<MbTimelineItem>();
        if (jArr != null) {
            // Read the activities in chronological order
            for (int index = jArr.length() - 1; index >= 0; index--) {
                try {
                    JSONObject jso = jArr.getJSONObject(index);
                    MbTimelineItem item = timelineItemFromJson(jso);
                    timeline.add(item);
                } catch (JSONException e) {
                    throw ConnectionException.loggedJsonException(this, "Parsing timeline", e, null);
                }
            }
        }
        MyLog.d(TAG, "getTimeline '" + url + "' " + timeline.size() + " messages");
        return timeline;
    }

    @Override
    public int fixedDownloadLimitForApiRoutine(int limit, ApiRoutineEnum apiRoutine) {
        final int maxLimit = apiRoutine == ApiRoutineEnum.GET_FRIENDS ? 200 : 20;
        int out = super.fixedDownloadLimitForApiRoutine(limit, apiRoutine);
        if (out > maxLimit) {
            out = maxLimit;
        }
        return out;
    }

    private MbTimelineItem timelineItemFromJson(JSONObject activity) throws ConnectionException {
        MbTimelineItem item = new MbTimelineItem();
        if (PumpioObjectType.ACTIVITY.isMyType(activity)) {
            try {
                item.timelineItemPosition = new TimelinePosition(activity.optString("id"));
                item.timelineItemDate = dateFromJson(activity, "updated");

                if (PumpioObjectType.PERSON.isMyType(activity.getJSONObject("object"))) {
                    item.mbUser = userFromJsonActivity(activity);
                } else {
                    item.mbMessage = messageFromJsonActivity(activity);
                }
            } catch (JSONException e) {
                throw ConnectionException.loggedJsonException(this, "Parsing timeline item", e, activity);
            }
        } else {
            MyLog.e(this, "Not an Activity in the timeline:" + activity.toString());
            item.mbMessage = messageFromJson(activity);
        }
        return item;
    }

    public MbUser userFromJsonActivity(JSONObject activity) throws ConnectionException {
        MbUser mbUser;
        try {
            String verb = activity.getString("verb");
            String oid = activity.optString("id");
            if (TextUtils.isEmpty(oid)) {
                MyLog.d(TAG, "Pumpio activity has no id:" + activity.toString(2));
                return MbUser.getEmpty();
            }
            mbUser = userFromJson(activity.getJSONObject("object"));
            if (activity.has("actor")) {
                mbUser.actor = userFromJson(activity.getJSONObject("actor"));
            }

            if ("follow".equalsIgnoreCase(verb)) {
                mbUser.followedByActor = TriState.TRUE;
            } else if ("unfollow".equalsIgnoreCase(verb) || "stop-following".equalsIgnoreCase(verb)) {
                mbUser.followedByActor = TriState.FALSE;
            }
        } catch (JSONException e) {
            throw ConnectionException.loggedJsonException(this, "Parsing activity", e, activity);
        }
        return mbUser;
    }

    MbMessage messageFromJson(JSONObject jso) throws ConnectionException {
        if (MyLog.isLoggable(this, MyLog.VERBOSE)) {
            try {
                MyLog.v(this, "messageFromJson: " + jso.toString(2));
            } catch (NullPointerException | JSONException e) {
                ConnectionException.loggedJsonException(this, "messageFromJson", e, jso);
            }
        }
        if (PumpioObjectType.ACTIVITY.isMyType(jso)) {
            return messageFromJsonActivity(jso);
        } else if (PumpioObjectType.compatibleWith(jso) == PumpioObjectType.COMMENT) {
            return messageFromJsonComment(jso);
        } else {
            return MbMessage.getEmpty();
        }
    }

    private MbMessage messageFromJsonActivity(JSONObject activity) throws ConnectionException {
        MbMessage message;
        try {
            String verb = activity.getString("verb");
            String oid = activity.optString("id");
            if (TextUtils.isEmpty(oid)) {
                MyLog.d(this, "Pumpio activity has no id:" + activity.toString(2));
                return MbMessage.getEmpty();
            }
            message = MbMessage.fromOriginAndOid(data.getOriginId(), oid);
            message.actor = MbUser.fromOriginAndUserOid(data.getOriginId(), data.getAccountUserOid());
            message.sentDate = dateFromJson(activity, "updated");

            if (activity.has("actor")) {
                message.sender = userFromJson(activity.getJSONObject("actor"));
                if (!message.sender.isEmpty()) {
                    message.actor = message.sender;
                }
            }
            if (activity.has("to")) {
                JSONObject to = activity.optJSONObject("to");
                if (to != null) {
                    message.recipient = userFromJson(to);
                } else {
                    JSONArray arrayOfTo = activity.optJSONArray("to");
                    if (arrayOfTo != null && arrayOfTo.length() > 0) {
                        // TODO: handle multiple recipients
                        to = arrayOfTo.optJSONObject(0);
                        MbUser recipient = userFromJson(to);
                        if (!recipient.isEmpty()) {
                            message.recipient = recipient;
                        }
                    }
                }
            }
            if (activity.has("generator")) {
                JSONObject generator = activity.getJSONObject("generator");
                if (generator.has("displayName")) {
                    message.via = generator.getString("displayName");
                }
            }

            JSONObject jso = activity.getJSONObject("object");
            // Is this a reblog ("Share" in terms of Activity streams)?
            if ("share".equalsIgnoreCase(verb)) {
                message.rebloggedMessage = messageFromJson(jso);
                if (message.rebloggedMessage.isEmpty()) {
                    MyLog.d(TAG, "No reblogged message " + jso.toString(2));
                    return message.markAsEmpty();
                }
            } else {
                if ("favorite".equalsIgnoreCase(verb)) {
                    message.favoritedByActor = TriState.TRUE;
                } else if ("unfavorite".equalsIgnoreCase(verb) || "unlike".equalsIgnoreCase(verb)) {
                    message.favoritedByActor = TriState.FALSE;
                }

                if (PumpioObjectType.compatibleWith(jso) == PumpioObjectType.COMMENT) {
                    parseComment(message, jso);
                } else {
                    return message.markAsEmpty();
                }
            }
        } catch (JSONException e) {
            throw ConnectionException.loggedJsonException(this, "Parsing activity", e, activity);
        }
        return message;
    }

    private void parseComment(MbMessage message, JSONObject jso) throws ConnectionException {
        try {
            String oid = jso.optString("id");
            if (!TextUtils.isEmpty(oid) && !message.oid.equalsIgnoreCase(oid)) {
                message.oid = oid;
            }
            if (jso.has("author")) {
                MbUser author = userFromJson(jso.getJSONObject("author"));
                if (!author.isEmpty()) {
                    message.sender = author;
                }
            }
            if (jso.has("content")) {
                message.setBody(jso.getString("content"));
            }
            message.sentDate = dateFromJson(jso, "published");

            if (jso.has("generator")) {
                JSONObject generator = jso.getJSONObject("generator");
                if (generator.has("displayName")) {
                    message.via = generator.getString("displayName");
                }
            }
            message.url = jso.optString("url");

            if (jso.has("fullImage") || jso.has("image")) {
                URL url = getImageUrl(jso, "fullImage");
                if (url == null) {
                    url = getImageUrl(jso, "image");
                }
                MbAttachment mbAttachment = MbAttachment.fromUrlAndContentType(url, MyContentType.IMAGE);
                if (mbAttachment.isValid()) {
                    message.attachments.add(mbAttachment);
                } else {
                    MyLog.d(this, "Invalid attachment; " + jso.toString());
                }
            }

            // If the Msg is a Reply to other message
            if (jso.has("inReplyTo")) {
                JSONObject inReplyToObject = jso.getJSONObject("inReplyTo");
                message.inReplyToMessage = messageFromJson(inReplyToObject);
            }
        } catch (JSONException e) {
            throw ConnectionException.loggedJsonException(this, "Parsing comment/note", e, jso);
        }
    }

    private URL getImageUrl(JSONObject jso, String imageTag) throws JSONException {
        if (jso.has(imageTag)) {
            JSONObject attachment = jso.getJSONObject(imageTag);
            return UrlUtils.fromJson(attachment, "url");
        }
        return null;
    }

    private MbMessage messageFromJsonComment(JSONObject jso) throws ConnectionException {
        MbMessage message;
        try {
            String oid = jso.optString("id");
            if (TextUtils.isEmpty(oid)) {
                MyLog.d(TAG, "Pumpio object has no id:" + jso.toString(2));
                return MbMessage.getEmpty();
            }
            message = MbMessage.fromOriginAndOid(data.getOriginId(), oid);
            message.actor = MbUser.fromOriginAndUserOid(data.getOriginId(), data.getAccountUserOid());

            parseComment(message, jso);
        } catch (JSONException e) {
            throw ConnectionException.loggedJsonException(this, "Parsing comment", e, jso);
        }
        return message;
    }

    /**
     * 2014-01-22 According to the crash reports, userId may not have "acct:" prefix
     */
    public String userOidToUsername(String userId) {
        String username = "";
        if (!TextUtils.isEmpty(userId)) {
            int indexOfColon = userId.indexOf(':');
            if (indexOfColon >= 0) {
                username = userId.substring(indexOfColon + 1);
            } else {
                username = userId;
            }
        }
        return username;
    }

    public String usernameToNickname(String username) {
        String nickname = "";
        if (!TextUtils.isEmpty(username)) {
            int indexOfAt = username.indexOf('@');
            if (indexOfAt > 0) {
                nickname = username.substring(0, indexOfAt);
            }
        }
        return nickname;
    }

    public String usernameToHost(String username) {
        String host = "";
        if (!TextUtils.isEmpty(username)) {
            int indexOfAt = username.indexOf('@');
            if (indexOfAt >= 0) {
                host = username.substring(indexOfAt + 1);
            }
        }
        return host;
    }

    @Override
    public List<MbTimelineItem> search(String searchQuery, int limit) throws ConnectionException {
        return new ArrayList<MbTimelineItem>();
    }

    @Override
    public MbUser followUser(String userId, Boolean follow) throws ConnectionException {
        return verbWithUser(follow ? "follow" : "stop-following", userId);
    }

    private MbUser verbWithUser(String verb, String userId) throws ConnectionException {
        return ActivitySender.fromId(this, userId).sendUser(verb);
    }

    @Override
    public MbUser getUser(String userId) throws ConnectionException {
        ConnectionAndUrl conu = getConnectionAndUrl(ApiRoutineEnum.GET_USER, userId);
        JSONObject jso = conu.httpConnection.getRequest(conu.url);
        MbUser mbUser = userFromJson(jso);
        MyLog.v(this, "getUser '" + userId + "' " + mbUser.realName);
        return mbUser;
    }
}