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

Java tutorial

Introduction

Here is the source code for org.andstatus.app.net.social.pumpio.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.pumpio;

import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;

import org.andstatus.app.context.MyContextHolder;
import org.andstatus.app.data.DownloadStatus;
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.net.social.Connection;
import org.andstatus.app.net.social.MbAttachment;
import org.andstatus.app.net.social.MbMessage;
import org.andstatus.app.net.social.MbRateLimitStatus;
import org.andstatus.app.net.social.MbTimelineItem;
import org.andstatus.app.net.social.MbUser;
import org.andstatus.app.net.social.TimelinePosition;
import org.andstatus.app.origin.OriginConnectionData;
import org.andstatus.app.util.JsonUtils;
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();
    static final String APPLICATION_ID = "http://andstatus.org/andstatus";

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

    @Override
    protected String getApiPath1(ApiRoutineEnum routine) {
        String url;
        switch (routine) {
        case ACCOUNT_VERIFY_CREDENTIALS:
            url = "whoami";
            break;
        case GET_FOLLOWERS:
        case GET_FOLLOWERS_IDS:
            url = "user/%nickname%/followers";
            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 (!ObjectType.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.setRealName(jso.optString("displayName"));
        user.avatarUrl = JsonUtils.optStringInside(jso, "image", "url");
        user.location = JsonUtils.optStringInside(jso, "location", "displayName");
        user.setDescription(jso.optString("summary"));
        user.setHomepage(jso.optString("url"));
        user.setProfileUrl(jso.optString("url"));
        user.setUpdatedDate(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]):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 actOnMessage(ActivityType.UNFAVORITE, messageId);
    }

    @Override
    public MbMessage createFavorite(String messageId) throws ConnectionException {
        return actOnMessage(ActivityType.FAVORITE, messageId);
    }

    @Override
    public boolean destroyStatus(String messageId) throws ConnectionException {
        MbMessage message = actOnMessage(ActivityType.DELETE, messageId);
        return !message.isEmpty();
    }

    private MbMessage actOnMessage(ActivityType activityType, String messageId) throws ConnectionException {
        return ActivitySender.fromId(this, messageId).sendMessage(activityType);
    }

    @Override
    public List<MbUser> getFollowers(String userId) throws ConnectionException {
        return getUsers(userId, ApiRoutineEnum.GET_FOLLOWERS);
    }

    @Override
    public List<MbUser> getFriends(String userId) throws ConnectionException {
        return getUsers(userId, ApiRoutineEnum.GET_FRIENDS);
    }

    @NonNull
    private List<MbUser> getUsers(String userId, ApiRoutineEnum apiRoutine) throws ConnectionException {
        int limit = 200;
        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> users = new ArrayList<>();
        if (jArr != null) {
            for (int index = 0; index < jArr.length(); index++) {
                try {
                    JSONObject jso = jArr.getJSONObject(index);
                    MbUser item = userFromJson(jso);
                    users.add(item);
                } catch (JSONException e) {
                    throw ConnectionException.loggedJsonException(this, "Parsing list of users", e, null);
                }
            }
        }
        MyLog.d(TAG, apiRoutine + " '" + url + "' " + users.size() + " users");
        return users;
    }

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

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

    private 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("/collection/") || oid.endsWith("/followers")) {
            objectType = "collection";
        } 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 {
        if (TextUtils.isEmpty(userId)) {
            throw new ConnectionException(StatusCode.BAD_REQUEST, apiRoutine + ": userId is required");
        }
        return getConnectionAndUrlForUsername(apiRoutine, userOidToUsername(userId));
    }

    private ConnectionAndUrl getConnectionAndUrlForUsername(ApiRoutineEnum apiRoutine, String username)
            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(username)) {
            throw new ConnectionException(StatusCode.BAD_REQUEST, apiRoutine + ": userName is required");
        }
        String nickname = usernameToNickname(username);
        if (TextUtils.isEmpty(nickname)) {
            throw new ConnectionException(StatusCode.BAD_REQUEST,
                    apiRoutine + ": wrong userName='" + username + "'");
        }
        String host = usernameToHost(username);
        conu.httpConnection = http;
        if (TextUtils.isEmpty(host)) {
            throw new ConnectionException(StatusCode.BAD_REQUEST,
                    apiRoutine + ": host is empty for the userName='" + username + "'");
        } 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.copy();
            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 statusId, String recipientId, Uri mediaUri)
            throws ConnectionException {
        String message = toHtmlIfAllowed(messageIn);
        ActivitySender sender = ActivitySender.fromContent(this, statusId, message);
        sender.setRecipient(recipientId);
        sender.setMediaUri(mediaUri);
        return messageFromJson(sender.sendMe(ActivityType.POST));
    }

    @Override
    public MbMessage postReblog(String rebloggedId) throws ConnectionException {
        return actOnMessage(ActivityType.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<>();
        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 (ObjectType.ACTIVITY.isMyType(activity)) {
            try {
                item.timelineItemPosition = new TimelinePosition(activity.optString("id"));
                item.timelineItemDate = dateFromJson(activity, "updated");

                if (ObjectType.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;
    }

    MbUser userFromJsonActivity(JSONObject activity) throws ConnectionException {
        MbUser mbUser;
        try {
            ActivityType activityType = ActivityType.load(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 (activityType.equals(ActivityType.FOLLOW)) {
                mbUser.followedByActor = TriState.TRUE;
            } else if (activityType.equals(ActivityType.STOP_FOLLOWING)) {
                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.isVerboseEnabled()) {
            try {
                MyLog.v(this, "messageFromJson: " + jso.toString(2));
            } catch (NullPointerException | JSONException e) {
                throw ConnectionException.loggedJsonException(this, "messageFromJson", e, jso);
            }
        }
        if (ObjectType.ACTIVITY.isMyType(jso)) {
            return messageFromJsonActivity(jso);
        } else if (ObjectType.compatibleWith(jso) == ObjectType.COMMENT) {
            return messageFromJsonComment(jso);
        } else {
            return MbMessage.getEmpty();
        }
    }

    private MbMessage messageFromJsonActivity(JSONObject activity) throws ConnectionException {
        MbMessage message;
        try {
            ActivityType activityType = ActivityType.load(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, DownloadStatus.LOADED);
            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(Properties.GENERATOR.code);
                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 (activityType.equals(ActivityType.SHARE)) {
                message.rebloggedMessage = messageFromJson(jso);
                if (message.rebloggedMessage.isEmpty()) {
                    MyLog.d(TAG, "No reblogged message " + jso.toString(2));
                    return message.markAsEmpty();
                }
            } else {
                if (activityType.equals(ActivityType.FAVORITE)) {
                    message.favoritedByActor = TriState.TRUE;
                } else if (activityType.equals(ActivityType.UNFAVORITE)) {
                    message.favoritedByActor = TriState.FALSE;
                }

                if (ObjectType.compatibleWith(jso) == ObjectType.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(Properties.GENERATOR.code)) {
                JSONObject generator = jso.getJSONObject(Properties.GENERATOR.code);
                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);
                message.inReplyToMessage.setSubscribed(TriState.FALSE);
            }

            if (jso.has("replies")) {
                JSONObject replies = jso.getJSONObject("replies");
                if (replies.has("items")) {
                    JSONArray jArr = replies.getJSONArray("items");
                    for (int index = 0; index < jArr.length(); index++) {
                        try {
                            MbMessage item = messageFromJson(jArr.getJSONObject(index));
                            item.setSubscribed(TriState.FALSE);
                            message.replies.add(item);
                        } catch (JSONException e) {
                            throw ConnectionException.loggedJsonException(this, "Parsing list of replies", e, null);
                        }
                    }
                }
            }
        } 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, DownloadStatus.LOADED);
            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(TimelinePosition youngestPosition, int limit, String searchQuery)
            throws ConnectionException {
        return new ArrayList<>();
    }

    @Override
    public MbUser followUser(String userId, Boolean follow) throws ConnectionException {
        return actOnUser(follow ? ActivityType.FOLLOW : ActivityType.STOP_FOLLOWING, userId);
    }

    private MbUser actOnUser(ActivityType activityType, String userId) throws ConnectionException {
        return ActivitySender.fromId(this, userId).sendUser(activityType);
    }

    @Override
    public MbUser getUser(String userId, String userName) throws ConnectionException {
        ConnectionAndUrl conu = getConnectionAndUrlForUsername(ApiRoutineEnum.GET_USER,
                MbUser.isOidReal(userId) ? userOidToUsername(userId) : userName);
        JSONObject jso = conu.httpConnection.getRequest(conu.url);
        MbUser mbUser = userFromJson(jso);
        MyLog.v(this, "getUser oid='" + userId + "', userName='" + userName + "' -> " + mbUser.getRealName());
        return mbUser;
    }

    protected OriginConnectionData getData() {
        return data;
    }

}