package com.malmstein.yahnac.data; import android.content.ContentValues; import android.text.TextUtils; import android.util.Pair; import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; import com.firebase.client.FirebaseError; import com.firebase.client.ValueEventListener; import com.malmstein.yahnac.comments.parser.CommentsParser; import com.malmstein.yahnac.comments.parser.VoteUrlParser; import com.malmstein.yahnac.injection.Inject; import com.malmstein.yahnac.model.Login; import com.malmstein.yahnac.model.OperationResponse; import com.malmstein.yahnac.model.Story; import com.novoda.notils.logger.simple.Log; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Vector; import org.jsoup.Connection; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import rx.Observable; import rx.Subscriber; import rx.functions.Func1; import rx.schedulers.Schedulers; public class HNewsApi { private static final String BAD_UPVOTE_RESPONSE = "Can't make that vote."; private static Element extractHmac(Document replyDocument) { return replyDocument .select("input[name=hmac]") .first(); } public Observable<List<ContentValues>> getStories(final Story.FILTER FILTER) { return Observable.create(new Observable.OnSubscribe<DataSnapshot>() { @Override public void call(final Subscriber<? super DataSnapshot> subscriber) { Firebase topStories = getStoryFirebase(FILTER); topStories.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { if (dataSnapshot != null) { subscriber.onNext(dataSnapshot); } else { Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: getStories is empty for " + FILTER.name()); } subscriber.onCompleted(); } @Override public void onCancelled(FirebaseError firebaseError) { Log.d(firebaseError.getCode()); } }); } }).flatMap(new Func1<DataSnapshot, Observable<Pair<Integer, Long>>>() { @Override public Observable<Pair<Integer, Long>> call(final DataSnapshot dataSnapshot) { return Observable.create(new Observable.OnSubscribe<Pair<Integer, Long>>() { @Override public void call(Subscriber<? super Pair<Integer, Long>> subscriber) { for (int i = 0; i < dataSnapshot.getChildrenCount(); i++) { Long id = (Long) dataSnapshot.child(String.valueOf(i)).getValue(); Integer rank = Integer.valueOf(dataSnapshot.child(String.valueOf(i)).getKey()); Pair<Integer, Long> storyRoot = new Pair<>(rank, id); subscriber.onNext(storyRoot); } subscriber.onCompleted(); } }); } }).flatMap(new Func1<Pair<Integer, Long>, Observable<ContentValues>>() { @Override public Observable<ContentValues> call(final Pair<Integer, Long> storyRoot) { return Observable.create(new Observable.OnSubscribe<ContentValues>() { @Override public void call(final Subscriber<? super ContentValues> subscriber) { final Firebase story = new Firebase("https://hacker-news.firebaseio.com/v0/item/" + storyRoot.second); story.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { Map<String, Object> newItem = (Map<String, Object>) dataSnapshot.getValue(); if (newItem != null) { ContentValues story = mapStory(newItem, FILTER, storyRoot.first); if (story != null) { subscriber.onNext(story); } else { subscriber.onNext(new ContentValues()); Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: onDataChange is empty in " + storyRoot.second); } } subscriber.onCompleted(); } @Override public void onCancelled(FirebaseError firebaseError) { Log.d(firebaseError.getCode()); Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: onCancelled " + firebaseError.getMessage()); subscriber.onCompleted(); } }); } }); } }) .toList(); } private ContentValues mapStory(Map<String, Object> map, Story.FILTER filter, Integer rank) { ContentValues storyValues = new ContentValues(); try { String by = (String) map.get("by"); Long id = (Long) map.get("id"); String type = (String) map.get("type"); Long time = (Long) map.get("time"); Long score = (Long) map.get("score"); String title = (String) map.get("title"); String url = (String) map.get("url"); Long descendants = Long.valueOf(0); if (map.get("descendants") != null) { descendants = (Long) map.get("descendants"); } storyValues.put(HNewsContract.StoryEntry.ITEM_ID, id); storyValues.put(HNewsContract.StoryEntry.BY, by); storyValues.put(HNewsContract.StoryEntry.TYPE, type); storyValues.put(HNewsContract.StoryEntry.TIME_AGO, time * 1000); storyValues.put(HNewsContract.StoryEntry.SCORE, score); storyValues.put(HNewsContract.StoryEntry.TITLE, title); storyValues.put(HNewsContract.StoryEntry.COMMENTS, descendants); storyValues.put(HNewsContract.StoryEntry.URL, url); storyValues.put(HNewsContract.StoryEntry.RANK, rank); storyValues.put(HNewsContract.StoryEntry.TIMESTAMP, System.currentTimeMillis()); storyValues.put(HNewsContract.StoryEntry.FILTER, filter.name()); } catch (Exception ex) { Log.d(ex.getMessage()); } return storyValues; } private Firebase getStoryFirebase(Story.FILTER FILTER) { switch (FILTER) { case show: return new Firebase("https://hacker-news.firebaseio.com/v0/showstories"); case ask: return new Firebase("https://hacker-news.firebaseio.com/v0/askstories"); case jobs: return new Firebase("https://hacker-news.firebaseio.com/v0/jobstories"); default: return new Firebase("https://hacker-news.firebaseio.com/v0/topstories"); } } Observable<Vector<ContentValues>> getCommentsFromStory(Long storyId) { return Observable.create( new CommentsUpdateOnSubscribe(storyId)) .subscribeOn(Schedulers.io()); } Observable<Login> login(String username, String password) { return Observable.create( new LoginOnSubscribe(username, password)) .subscribeOn(Schedulers.io()); } Observable<OperationResponse> vote(Story storyId) { return Observable.create( new ParseVoteUrlOnSubscribe(storyId.getId())) .flatMap(new Func1<String, Observable<OperationResponse>>() { @Override public Observable<OperationResponse> call(final String voteUrl) { return Observable.create(new Observable.OnSubscribe<OperationResponse>() { @Override public void call(Subscriber<? super OperationResponse> subscriber) { if (voteUrl.equals(VoteUrlParser.EMPTY)) { subscriber.onNext(OperationResponse.FAILURE); } try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Connection.Response response = connectionProvider .voteConnection(voteUrl) .execute(); if (response.statusCode() == 200) { if (response.body() == null) { subscriber.onError(new Throwable("")); } Document doc = response.parse(); String text = doc.text(); if (text.equals(BAD_UPVOTE_RESPONSE)) { subscriber.onNext(OperationResponse.FAILURE); } else { subscriber.onNext(OperationResponse.SUCCESS); } } else { subscriber.onNext(OperationResponse.FAILURE); } } catch (IOException e) { subscriber.onError(e); } } }); } }) .subscribeOn(Schedulers.io()); } Observable<OperationResponse> commentOnStory(final Long itemId, final String comment) { return Observable.create( new ParseHmacOnSubscribe(itemId)) .flatMap(new Func1<String, Observable<OperationResponse>>() { @Override public Observable<OperationResponse> call(final String hmac) { return Observable.create(new Observable.OnSubscribe<OperationResponse>() { @Override public void call(Subscriber<? super OperationResponse> subscriber) { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Request request = connectionProvider .commentOnStoryRequest(String.valueOf(itemId), comment, hmac); OkHttpClient client = new OkHttpClient(); Response response = client.newCall(request).execute(); if (response.code() == 200) { subscriber.onNext(OperationResponse.SUCCESS); } else { subscriber.onNext(OperationResponse.FAILURE); } } catch (IOException e) { subscriber.onError(e); } } }); } }) .subscribeOn(Schedulers.io()); } Observable<OperationResponse> replyToComment(final Long storyId, final long commentId, final String comment) { return Observable.create( new ParseReplyHmacOnSubscribe(storyId, commentId)) .flatMap(new Func1<String, Observable<OperationResponse>>() { @Override public Observable<OperationResponse> call(final String hmac) { return Observable.create(new Observable.OnSubscribe<OperationResponse>() { @Override public void call(Subscriber<? super OperationResponse> subscriber) { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Request request = connectionProvider .replyToCommentRequest(String.valueOf(storyId), String.valueOf(commentId), comment, hmac); OkHttpClient client = new OkHttpClient(); Response response = client.newCall(request).execute(); if (response.code() == 200) { subscriber.onNext(OperationResponse.SUCCESS); } else { subscriber.onNext(OperationResponse.FAILURE); } } catch (IOException e) { subscriber.onError(e); } } }); } }) .subscribeOn(Schedulers.io()); } private static class CommentsUpdateOnSubscribe implements Observable.OnSubscribe<Vector<ContentValues>> { private final Long storyId; private Subscriber<? super Vector<ContentValues>> subscriber; private CommentsUpdateOnSubscribe(Long storyId) { this.storyId = storyId; } @Override public void call(Subscriber<? super Vector<ContentValues>> subscriber) { this.subscriber = subscriber; startFetchingComments(); subscriber.onCompleted(); } private void startFetchingComments() { Vector<ContentValues> commentsList = new Vector<>(); try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Document commentsDocument = connectionProvider .commentsConnection(storyId) .get(); commentsList = new CommentsParser(storyId, commentsDocument).parse(); } catch (IOException e) { subscriber.onError(e); } subscriber.onNext(commentsList); } } private static class LoginOnSubscribe implements Observable.OnSubscribe<Login> { private final String username; private final String password; private Subscriber<? super Login> subscriber; private LoginOnSubscribe(String username, String password) { this.username = username; this.password = password; } @Override public void call(Subscriber<? super Login> subscriber) { this.subscriber = subscriber; attemptLogin(); subscriber.onCompleted(); } private void attemptLogin() { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Connection.Response response = connectionProvider .loginConnection(username, password) .execute(); String cookie = response.cookie("user"); String cfduid = response.cookie("_cfduid"); if (!TextUtils.isEmpty(cookie)) { subscriber.onNext(new Login(username, cookie, Login.Status.SUCCESSFUL)); } else { subscriber.onNext(new Login(username, null, Login.Status.WRONG_CREDENTIALS)); } } catch (IOException e) { subscriber.onError(e); } } } private static class ParseVoteUrlOnSubscribe implements Observable.OnSubscribe<String> { private final Long storyId; private Subscriber<? super String> subscriber; private ParseVoteUrlOnSubscribe(Long storyId) { this.storyId = storyId; } @Override public void call(Subscriber<? super String> subscriber) { this.subscriber = subscriber; startFetchingVoteUrl(); subscriber.onCompleted(); } private void startFetchingVoteUrl() { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Document commentsDocument = connectionProvider .commentsConnection(storyId) .get(); String voteUrl = new VoteUrlParser(commentsDocument, storyId).parse(); if (voteUrl.equals("/null")) { subscriber.onError(new LoggedOutException()); } else { subscriber.onNext(voteUrl); } } catch (IOException e) { subscriber.onError(e); } } } private static class ParseHmacOnSubscribe implements Observable.OnSubscribe<String> { private final Long storyId; private Subscriber<? super String> subscriber; private ParseHmacOnSubscribe(Long storyId) { this.storyId = storyId; } @Override public void call(Subscriber<? super String> subscriber) { this.subscriber = subscriber; startFetchingHmac(); subscriber.onCompleted(); } private void startFetchingHmac() { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Document replyDocument = connectionProvider .commentsConnection(storyId) .get(); Element replyInput = extractHmac(replyDocument); if (replyInput != null) { String replyFnid = replyInput.attr("value"); subscriber.onNext(replyFnid); } else { subscriber.onError(new LoggedOutException()); } } catch (IOException e) { subscriber.onError(e); } } } private static class ParseReplyHmacOnSubscribe implements Observable.OnSubscribe<String> { private final Long storyId; private final Long commentId; private Subscriber<? super String> subscriber; private ParseReplyHmacOnSubscribe(Long storyId, Long commentId) { this.storyId = storyId; this.commentId = commentId; } @Override public void call(Subscriber<? super String> subscriber) { this.subscriber = subscriber; startFetchingHmac(); subscriber.onCompleted(); } private void startFetchingHmac() { try { ConnectionProvider connectionProvider = Inject.connectionProvider(); Document replyDocument = connectionProvider .replyCommentConnection(storyId, commentId) .get(); Element replyInput = extractHmac(replyDocument); if (replyInput != null) { String hmac = replyInput.attr("value"); subscriber.onNext(hmac); } else { subscriber.onError(new LoggedOutException()); } } catch (IOException e) { subscriber.onError(e); } } } }