package com.leavjenn.hews.data.remote;
import android.content.SharedPreferences;
import android.text.Html;
import android.util.Log;
import com.leavjenn.hews.Constants;
import com.leavjenn.hews.misc.SharedPrefsManager;
import com.leavjenn.hews.model.Comment;
import com.leavjenn.hews.model.HNItem;
import com.leavjenn.hews.model.Post;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Func1;
public class DataManager {
public static final String HACKER_NEWS_BASE_URL = "https://news.ycombinator.com/";
public static final String HACKER_NEWS_ITEM_URL = "https://news.ycombinator.com/item?id=";
private static final int MINIMUM_STRING = 20;
HackerNewsService mHackerNewsService, mSearchService;
public DataManager() {
mHackerNewsService = new RetrofitHelper().getHackerNewsService();
mSearchService = new RetrofitHelper().getSearchService();
}
public Observable<List<Long>> getPostList(String type) {
return mHackerNewsService.getStories(type);
}
public Observable<Post> getPost(Long postId) {
return mHackerNewsService.getStory(String.valueOf(postId));
}
public Observable<Post> getPostsn(List<Long> postIds, boolean isByOrder) {
if (isByOrder) {
return Observable.from(postIds)
.concatMap(new Func1<Long, Observable<Post>>() {
@Override
public Observable<Post> call(Long aLong) {
return mHackerNewsService.getStory(String.valueOf(aLong));
}
})
.onErrorReturn(new Func1<Throwable, Post>() {
@Override
public Post call(Throwable throwable) {
Log.e("getPosts", throwable.toString());
return null;
}
})
.filter(new Func1<Post, Boolean>() {
@Override
public Boolean call(Post post) {
return post != null && post.getTitle() != null;
}
});
} else {
return Observable.from(postIds)
.flatMap(new Func1<Long, Observable<Post>>() {
@Override
public Observable<Post> call(Long aLong) {
return mHackerNewsService.getStory(String.valueOf(aLong));
}
})
.onErrorReturn(new Func1<Throwable, Post>() {
@Override
public Post call(Throwable throwable) {
Log.e("getPosts", throwable.toString());
return null;
}
})
.filter(new Func1<Post, Boolean>() {
@Override
public Boolean call(Post post) {
return post != null && post.getTitle() != null;
}
});
}
}
public Observable<Post> getPosts(List<Long> postIds) {
if (postIds.size() > Constants.NUM_LOADING_ITEMS_SPLIT * 2) {
return Observable.concat(
getSubListPosts(postIds.subList(0, Constants.NUM_LOADING_ITEMS_SPLIT)),
getSubListPosts(postIds.subList(Constants.NUM_LOADING_ITEMS_SPLIT,
Constants.NUM_LOADING_ITEMS_SPLIT * 2)),
getSubListPosts(postIds.subList(Constants.NUM_LOADING_ITEMS_SPLIT * 2,
postIds.size())))
.flatMap(new Func1<List<Post>, Observable<Post>>() {
@Override
public Observable<Post> call(List<Post> posts) {
return Observable.from(posts);
}
});
} else {
return getSubListPosts(postIds)
.flatMap(new Func1<List<Post>, Observable<Post>>() {
@Override
public Observable<Post> call(List<Post> posts) {
return Observable.from(posts);
}
});
}
}
public Observable<List<Post>> getSubListPosts(final List<Long> postIds) {
return Observable.from(postIds)
.flatMap(new Func1<Long, Observable<Post>>() {
@Override
public Observable<Post> call(Long aLong) {
return mHackerNewsService.getStory(String.valueOf(aLong));
}
})
.onErrorReturn(new Func1<Throwable, Post>() {
@Override
public Post call(Throwable throwable) {
Log.e("getPosts", throwable.toString());
return null;
}
})
.filter(new Func1<Post, Boolean>() {
@Override
public Boolean call(Post post) {
return post != null && post.getTitle() != null;
}
})
.toList()
.map(new Func1<List<Post>, List<Post>>() {
@Override
public List<Post> call(List<Post> posts) {
return sortPosts(postIds, posts);
}
});
}
private List<Post> sortPosts(List<Long> postIds, List<Post> postList) {
HashMap<Long, Post> postsMap = new HashMap<>();
List<Post> orderedPostList = new ArrayList<>();
for (Post post : postList) {
postsMap.put(post.getId(), post);
}
for (Long id : postIds) {
orderedPostList.add(postsMap.get(id));
}
return orderedPostList;
}
public Observable<Comment> getSummary(List<Long> commentIds) {
return Observable.from(commentIds)
// if use flatMap(), there would be too many concurrent calls
.concatMap(new Func1<Long, Observable<Comment>>() {
@Override
public Observable<Comment> call(Long aLong) {
return mHackerNewsService.getComment(aLong);
}
}).filter(new Func1<Comment, Boolean>() {
@Override
public Boolean call(Comment comment) {
if (comment != null
&& comment.getBy() != null && !comment.getBy().trim().isEmpty()
&& comment.getText() != null && !comment.getText().trim().isEmpty()
&& comment.getText().length() > MINIMUM_STRING * 4) {
//TODO length num improve needed.
String s = Html.fromHtml(comment.getText()).toString().substring(0,
MINIMUM_STRING)
.toLowerCase();
return s.contains("summary")
|| s.contains("tldr")
|| s.contains("tl;dr")
|| s.contains("tl; dr");
} else {
return false;
}
}
}).firstOrDefault(null);
}
public Observable<List<Comment>> getComments(Post post, int level) {
List<Long> commentIds = post.getKids();
long descendants = post.getDescendants();
if (commentIds.size() > 3 && descendants > 15) {
Log.i("---getComments", "kids > 3");
return Observable.concat(getCommentsByBranches(commentIds.subList(0, 3), level),
getCommentsAllAtOnce(commentIds.subList(3, commentIds.size()), level));
} else if (descendants / commentIds.size() > 15) {
Log.i("---getComments", "few kids, big branch");
return getCommentsByBranches(commentIds, level);
} else {
Log.i("---getComments", "other");
return getCommentsAllAtOnce(commentIds, level);
}
}
public Observable<List<Comment>> getCommentsByBranches(List<Long> commentIds, final int level) {
Log.i("---", "getCommentsByBranches");
return Observable.from(commentIds)
.flatMap(new Func1<Long, Observable<List<Comment>>>() {
@Override
public Observable<List<Comment>> call(Long commentId) {
return getOneBranchComments(commentId, level);
}
});
}
public Observable<List<Comment>> getOneBranchComments(final long commentId, final int level) {
return mHackerNewsService.getComment(commentId)
.onErrorReturn(new Func1<Throwable, Comment>() {
@Override
public Comment call(Throwable throwable) {
Log.e("getOneBranchComments", throwable.toString());
return null;
}
})
.filter(new Func1<Comment, Boolean>() {
@Override
public Boolean call(Comment comment) {
return (comment != null) && !comment.getDeleted() && comment.getText() != null;
}
})
.flatMap(new Func1<Comment, Observable<Comment>>() {
@Override
public Observable<Comment> call(Comment comment) {
Log.i("---getOneBranchComments", String.valueOf(comment.getCommentId()));
return getInnerComments(comment, level);
}
})
.toList()
.map(new Func1<List<Comment>, List<Comment>>() {
@Override
public List<Comment> call(List<Comment> allComments) {
List<Long> firstLevelCommentAsList = new ArrayList<>();
firstLevelCommentAsList.add(commentId);
return sortComments(firstLevelCommentAsList, allComments);
}
});
}
public Observable<List<Comment>> getCommentsAllAtOnce(final List<Long> firstLevelCommentIds, final int level) {
Log.i("---", "getCommentsAllAtOnce");
return Observable.from(firstLevelCommentIds)
.flatMap(new Func1<Long, Observable<Comment>>() {
@Override
public Observable<Comment> call(Long commentId) {
return mHackerNewsService.getComment(commentId)
.onErrorReturn(new Func1<Throwable, Comment>() {
@Override
public Comment call(Throwable throwable) {
Log.e("getCommentsAllAtOnce", throwable.toString());
return null;
}
});
}
})
.filter(new Func1<Comment, Boolean>() {
@Override
public Boolean call(Comment comment) {
return (comment != null) && !comment.getDeleted() && comment.getText() != null;
}
})
.flatMap(new Func1<Comment, Observable<Comment>>() {
@Override
public Observable<Comment> call(Comment comment) {
return getInnerComments(comment, level);
}
})
.toList()
.map(new Func1<List<Comment>, List<Comment>>() {
@Override
public List<Comment> call(List<Comment> allComments) {
return sortComments(firstLevelCommentIds, allComments);
}
});
}
private Observable<Comment> getInnerComments(Comment comment, final int level) {
if (comment == null || comment.getDeleted() || comment.getText() == null) {
return null;
}
comment.setLevel(level);
if (comment.getKids() != null && !comment.getKids().isEmpty()) {
return Observable.just(comment)
.mergeWith(Observable.from(comment.getKids())
.flatMap(new Func1<Long, Observable<Comment>>() {
@Override
public Observable<Comment> call(Long commentId) {
return mHackerNewsService.getComment(commentId)
.onErrorReturn(new Func1<Throwable, Comment>() {
@Override
public Comment call(Throwable throwable) {
Log.e("getInnerComments", throwable.toString());
return null;
}
});
}
})
.filter(new Func1<Comment, Boolean>() {
@Override
public Boolean call(Comment comment) {
return (comment != null) && !comment.getDeleted()
&& comment.getText() != null;
}
})
.flatMap(new Func1<Comment, Observable<Comment>>() {
@Override
public Observable<Comment> call(Comment comment) {
return getInnerComments(comment, level + 1);
}
})
);
}
return Observable.just(comment);
}
private List<Comment> sortComments(List<Long> firstLevelCommentIds, List<Comment> allComments) {
HashMap<Long, Comment> allCommentsMap = new HashMap<>();
for (Comment childComment : allComments) {
allCommentsMap.put(childComment.getCommentId(), childComment);
}
List<Comment> validFirstLevelCommentList = new ArrayList<>();
for (Long id : firstLevelCommentIds) {
Comment firstLevelComment = allCommentsMap.get(id);
if (firstLevelComment != null && !firstLevelComment.getDeleted()
&& firstLevelComment.getText() != null) {
validFirstLevelCommentList.add(firstLevelComment);
}
}
return sortAllComments(validFirstLevelCommentList, allCommentsMap);
}
private List<Comment> sortAllComments(List<Comment> commentList, HashMap<Long, Comment> allCommentsMap) {
List<Comment> sortedList = new ArrayList<>();
for (Comment comment : commentList) {
sortedList.add(comment);
if (comment.getKids() != null && comment.getKids().size() > 0) {
List<Comment> validChildCommentList = new ArrayList<>();
for (long id : comment.getKids()) {
Comment childComment = allCommentsMap.get(id);
if (childComment != null && !childComment.getDeleted() && childComment.getText() != null) {
validChildCommentList.add(childComment);
}
}
sortedList.addAll(sortAllComments(validChildCommentList, allCommentsMap));
}
}
return sortedList;
}
public Observable<HNItem.SearchResult> getPopularPosts(String startTime, int page) {
return mSearchService.searchPopularity(startTime, page, Constants.NUM_LOADING_ITEMS);
}
public Observable<HNItem.SearchResult> getSearchResult(String keyword, String timeRange,
int page, boolean isSortByDate) {
if (isSortByDate) {
return mSearchService.searchByDate(keyword, timeRange, page, Constants.NUM_LOADING_ITEMS);
} else {
return mSearchService.search(keyword, timeRange, page, Constants.NUM_LOADING_ITEMS);
}
}
public Observable<String> login(final String username, final String password) {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try {
Connection login = Jsoup.connect(HACKER_NEWS_BASE_URL + "login");
login.header("Accept-Encoding", "gzip")
.data("go_to", "news")
.data("acct", username)
.data("pw", password)
.header("Origin", "https://news.ycombinator.com")
.followRedirects(true)
.referrer(HACKER_NEWS_BASE_URL + "login?go_to=news")
.method(Connection.Method.POST);
Connection.Response response = login.execute();
String cookie = response.cookie("user");
if (cookie == null) {
subscriber.onNext("");
} else {
subscriber.onNext(cookie);
}
} catch (Exception e) {
subscriber.onError(e);
}
}
});
}
public Observable<Integer> vote(final long itemId, final int voteState, final SharedPreferences sp) {
return Observable.create(new Observable.OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
String cookieLogin = SharedPrefsManager.getLoginCookie(sp);
if (cookieLogin.isEmpty()) {
subscriber.onNext(Constants.OPERATE_ERROR_NO_COOKIE);
subscriber.onCompleted();
}
try {
Connection vote = Jsoup.connect(HACKER_NEWS_ITEM_URL + itemId)
.header("Accept-Encoding", "gzip")
.cookie("user", cookieLogin);
Document commentsDocument = vote.get();
/*
votable element:
<a id="up_10276091"
href="vote?for=10276091&dir=up&auth=3ecc4be748d7cc412223ea906b559ce72ccdc262&goto=news"
onclick="return vote(this)">
url:
https://news.ycombinator.com/vote?for=10276091&dir=up
&auth=3ecc4be748d7cc412223ea906b559ce72ccdc262&goto=news
logout element:
<a id="up_10276091" href="vote?for=10276091&dir=up&goto=item%3Fid%3D10276091">
url:
https://news.ycombinator.com/vote?for=10276091&dir=up&goto=item%3Fid%3D10276091
*/
Elements links;
if (voteState == Constants.VOTE_DOWN) {
links = commentsDocument.select("a[id=down_" + itemId + "]");
} else {
links = commentsDocument.select("a[id=up_" + itemId + "]");
}
if (links.size() == 0) {
if (voteState == Constants.VOTE_DOWN) {
subscriber.onNext(Constants.OPERATE_ERROR_NOT_ENOUGH_KARMA);
} else {
subscriber.onNext(Constants.OPERATE_ERROR_HAVE_VOTED);
}
subscriber.onCompleted();
} else {
Element voteElement = links.get(0).select("a[href^=vote]").first();
if (!voteElement.attr("href").contains("auth=")) {
subscriber.onNext(Constants.OPERATE_ERROR_COOKIE_EXPIRED);
subscriber.onCompleted();
}
if (voteElement.attr("href").contains("auth=")) {
String url = (voteElement.attr("href"));
Request voteRequest = new Request.Builder()
.addHeader("cookie", "user=" + cookieLogin)
.url(HACKER_NEWS_BASE_URL + url)
.build();
Response response = new OkHttpClient().newCall(voteRequest).execute();
if (response.code() == 200) {
if (response.body() == null) {
subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
} else {
subscriber.onNext(Constants.OPERATE_SUCCESS);
}
} else {
subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
}
}
}
} catch (Exception e) {
subscriber.onError(e);
}
}
});
}
public Observable<Integer> reply(final long itemId, final String replyText, final String cookieLogin) {
return Observable.create(new Observable.OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
if (cookieLogin.isEmpty()) {
subscriber.onNext(Constants.OPERATE_ERROR_NO_COOKIE);
subscriber.onCompleted();
}
try {
Connection reply = Jsoup.connect(HACKER_NEWS_ITEM_URL + itemId)
.header("Accept-Encoding", "gzip")
.cookie("user", cookieLogin);
Document replyDocument = reply.get();
Element element = replyDocument.select("input[name=hmac]").first();
if (element != null) {
String replyHmac = element.attr("value");
RequestBody requestBody = (new FormEncodingBuilder())
.add("parent", String.valueOf(itemId))
.add("goto", (new StringBuilder()).append("item?id=").append(itemId).toString())
.add("hmac", replyHmac)
.add("text", replyText)
.build();
Request request = new Request.Builder()
.addHeader("cookie", "user=" + cookieLogin)
.url(HACKER_NEWS_BASE_URL + "comment")
.post(requestBody)
.build();
Response response = new OkHttpClient().newCall(request).execute();
if (response.code() == 200) {
subscriber.onNext(Constants.OPERATE_SUCCESS);
} else {
subscriber.onNext(Constants.OPERATE_ERROR_UNKNOWN);
}
} else {
subscriber.onNext(Constants.OPERATE_ERROR_COOKIE_EXPIRED);
}
} catch (Exception e) {
subscriber.onError(e);
}
}
});
}
}