package net.ggelardi.flucso.serv;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ggelardi.flucso.serv.Commons.PK;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import retrofit.Callback;
import retrofit.RequestInterceptor;
import retrofit.RestAdapter;
import retrofit.client.Request;
import retrofit.client.UrlConnectionClient;
import retrofit.http.Body;
import retrofit.http.EncodedPath;
import retrofit.http.GET;
import retrofit.http.POST;
import retrofit.http.Path;
import retrofit.http.Query;
import retrofit.mime.MultipartTypedOutput;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Base64;
import com.google.gson.annotations.SerializedName;
public class FFAPI {
private static final String API_URL = "http://friendfeed-api.com/v2";
private static final String API_KEY = "c914bd31ea024b9bade1365cefa8b989";
// private static final String API_SEC = "d5d5e78a0ced4a1da49230fe09696353078d9f37b0a841a888e24c064e88212d";
public interface FF {
// async
@GET("/feedinfo/{feed_id}")
void get_profile(@EncodedPath("feed_id") String feed_id, Callback<FeedInfo> callback);
@GET("/feedlist")
void get_navigation(Callback<FeedList> callback);
@GET("/short/{short_url}")
void rev_short(@Path("short_url") String short_url, Callback<Entry> callback);
@GET("/entry/{entry_id}")
void get_entry_async(@EncodedPath("entry_id") String entry_id, Callback<Entry> callback);
@POST("/short")
void get_short(@Query("entry") String entry_id, Callback<Entry> callback);
@POST("/entry")
void ins_entry(@Body MultipartTypedOutput mto, Callback<Entry> callback);
@POST("/entry/delete")
void del_entry(@Query("id") String entry_id, Callback<Entry> callback);
@POST("/entry/delete")
void und_entry(@Query("id") String entry_id, @Query("undelete") int undelete, Callback<Entry> callback);
@POST("/hide")
void set_hidden(@Query("entry") String entry_id, Callback<Entry> callback);
@POST("/hide")
void set_unhide(@Query("entry") String entry_id, @Query("unhide") int unhide, Callback<Entry> callback);
@POST("/like")
void ins_like(@Query("entry") String entry_id, Callback<Like> callback);
@POST("/like/delete")
void del_like(@Query("entry") String entry_id, Callback<SimpleResponse> callback);
@POST("/comment")
void ins_comment(@Query("entry") String entry_id, @Query("body") String body, Callback<Comment> callback);
@POST("/comment")
void upd_comment(@Query("entry") String entry_id, @Query("id") String comm_id, @Query("body") String body,
Callback<Comment> callback);
@POST("/comment/delete")
void del_comment(@Query("id") String comm_id, Callback<Comment> callback);
@POST("/comment/delete")
void und_comment(@Query("id") String comm_id, @Query("undelete") int undelete, Callback<Comment> callback);
@POST("/subscribe")
void subscribe(@Query("feed") String feed, @Query("list") String list, Callback<SimpleResponse> callback);
@POST("/unsubscribe")
void unsubscribe(@Query("feed") String feed, @Query("list") String list, Callback<SimpleResponse> callback);
// sync
@GET("/feedinfo/{feed_id}")
FeedInfo get_profile_sync(@Path("feed_id") String feed_id);
@GET("/feedlist")
FeedList get_navigation_sync();
@GET("/feed/{feed_id}")
Feed get_feed_normal(@EncodedPath("feed_id") String feed_id, @Query("start") int start, @Query("num") int num);
@GET("/updates/feed/{feed_id}")
Feed get_feed_updates(@EncodedPath("feed_id") String feed_id, @Query("num") int num,
@Query("cursor") String cursor, @Query("timeout") int timeout, @Query("updates") int updates);
@GET("/search")
Feed get_search_normal(@Query("q") String query, @Query("start") int start, @Query("num") int num);
@GET("/updates/search")
Feed get_search_updates(@Query("q") String query, @Query("num") int num, @Query("cursor") String cursor,
@Query("timeout") int timeout, @Query("updates") int updates);
@GET("/entry/{entry_id}")
Entry get_entry(@EncodedPath("entry_id") String entry_id);
}
public static class SimpleResponse {
public boolean success = false; // unlike & unsubscribe?
public String status = ""; // subscribe & unsubscribe?
}
static class IdentItem {
public static String accountID = "";
public static String accountName = "You";
public static String accountFeed = "Your feed";
public String id = "";
public List<String> commands = new ArrayList<String>();
public long timestamp = System.currentTimeMillis();
public long getAge() {
return System.currentTimeMillis() - timestamp;
}
public boolean isIt(String checkId) {
return id.trim().toLowerCase(Locale.getDefault()).equals(checkId.trim().toLowerCase(Locale.getDefault()));
}
}
public static class BaseFeed extends IdentItem {
public String name;
public String type = "";
public String description;
@SerializedName("private")
public Boolean locked = false;
public boolean isMe() {
return !TextUtils.isEmpty(accountID) && isIt(accountID);
}
public boolean isHome() {
return isIt("home");
}
public boolean isList() {
return type.equals("special") && id.startsWith("list/");
}
public boolean isUser() {
return type.equals("user");
}
public boolean isGroup() {
return type.equals("group");
}
public boolean canPost() {
return commands.contains("post");
}
public boolean canDM() {
return commands.contains("dm");
}
public boolean canSetSubscriptions() {
return isList() || (commands.contains("subscribe") || commands.contains("unsubscribe"));
}
public boolean isSubscribed() {
return commands.contains("unsubscribe");
}
public String getName() {
return isMe() ? accountName : name.trim();
}
public String getAvatarUrl() {
return "http://friendfeed-api.com/v2/picture/" + id + "?size=large";
}
}
public static class Feed extends BaseFeed {
public List<Entry> entries = new ArrayList<Entry>();
public Realtime realtime;
public static String lastDeletedEntry = "";
public int indexOf(String eid) {
for (int i = 0; i < entries.size(); i++)
if (entries.get(i).isIt(eid))
return i;
return -1;
}
public Entry find(String eid) {
for (Entry e : entries)
if (e.id.equals(eid))
return e;
return null;
}
public int append(Feed feed) {
int res = 0;
for (Entry e : feed.entries)
if (find(e.id) == null) {
entries.add(e);
e.checkLocalHide();
res++;
}
return res;
}
public int update(Feed feed, boolean sortLikeSite) {
realtime = feed.realtime;
timestamp = feed.timestamp;
int res = 0;
for (Entry e : feed.entries)
if (e.created) {
entries.add(0, e);
e.checkLocalHide();
res++;
} else
for (Entry old : entries)
if (old.isIt(e.id)) {
old.update(e);
break;
}
if (!TextUtils.isEmpty(lastDeletedEntry))
for (int n = 0; n < entries.size(); n++)
if (entries.get(n).id.equals(lastDeletedEntry)) {
entries.remove(n);
lastDeletedEntry = "";
break;
}
if (sortLikeSite && res > 0) {
Collections.sort(entries, new Comparator<BaseEntry>() {
@Override
public int compare(BaseEntry o1, BaseEntry o2) {
return o1.date.compareTo(o2.date) * -1;
}
});
}
return res;
}
public int update(Feed feed) {
return update(feed, false);
}
public void checkLocalHide() {
for (Entry e : entries)
e.checkLocalHide();
}
public static class Realtime {
public String cursor;
public final long timestamp = System.currentTimeMillis();
}
}
public static class BaseEntry extends IdentItem {
public BaseFeed from;
public Date date;
public String body;
public String rawBody;
public Origin via;
public boolean created;
public boolean updated;
public boolean banned = false; // local
public boolean spoiler = false; // local
public boolean isMine() {
return from.isMe();
}
public boolean canEdit() {
return commands.contains("edit");
}
public boolean canDelete() {
return commands.contains("delete");
}
public String getFuzzyTime() {
return DateUtils.getRelativeTimeSpanString(date.getTime(), System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS).toString();
}
public String getFirstUrl() {
String res = Commons.firstUrl(rawBody);
if (res == null)
res = Commons.firstUrl(body);
return res;
}
public String getFirstImage() {
String res = Commons.firstImage(rawBody);
if (res == null)
res = Commons.firstImage(body);
return res;
}
public void update(BaseEntry item) {
if (!isIt(item.id))
return;
timestamp = item.timestamp;
commands = item.commands;
from = item.from; // people and rooms can change name/avatar at any time.
via = item.via; // barely useless.
if (item.updated) {
date = item.date;
body = item.body;
rawBody = item.rawBody;
updated = true;
}
checkLocalHide();
}
public void checkLocalHide() {
banned = false;
spoiler = false;
try {
String chk = !TextUtils.isEmpty(body) ? body.toLowerCase(Locale.getDefault()).trim() : "";
chk += !TextUtils.isEmpty(chk) ? "\n\n\n" : "";
chk += !TextUtils.isEmpty(rawBody) ? rawBody.toLowerCase(Locale.getDefault()).trim() : "";
if (!TextUtils.isEmpty(chk)) {
if (!isMine())
for (String bw: Commons.bWords)
if (chk.indexOf(bw) >= 0) {
banned = true;
break;
}
spoiler = Commons.bSpoilers && chk.contains("#spoiler");
}
} catch (Exception err) {
}
}
public static class Origin {
public String name;
public String url;
}
}
public static class Entry extends BaseEntry {
public String rawLink;
public String url;
public List<Comment> comments = new ArrayList<Comment>();
public List<Like> likes = new ArrayList<Like>();
public BaseFeed[] to = new BaseFeed[] {};
public Thumbnail[] thumbnails = new Thumbnail[] {};
public Fof fof;
public String fofHtml;
public String shortId = "";
public String shortUrl = "";
public Attachment[] files = new Attachment[] {};
public Coordinates geo;
public boolean hidden = false; // undocumented
public int thumbpos = 0; // local
@Override
public void update(BaseEntry item) {
super.update(item);
if (!isIt(item.id))
return;
Entry entry = (Entry) item;
url = entry.url;
fof = entry.fof;
hidden = entry.hidden;
rawLink = entry.rawLink;
fofHtml = entry.fofHtml;
thumbnails = entry.thumbnails;
files = entry.files;
geo = entry.geo;
for (Comment comm : entry.comments)
if (comm.created)
comments.add(comm);
else
for (Comment old : comments)
if (old.isIt(comm.id)) {
old.update(comm);
break;
}
for (Like like : entry.likes)
if (like.created)
likes.add(like);
updated = true;
}
@Override
public void checkLocalHide() {
super.checkLocalHide();
if (canUnlike()) {
banned = false;
spoiler = false;
} else
for (BaseFeed bf: to)
if (Commons.bFeeds.contains(bf.id)) {
banned = true;
break;
}
for (Comment c: comments)
c.checkLocalHide();
}
public boolean isDM() {
if (to.length <= 0)
return false;
for (BaseFeed f: to)
if (!f.isUser() || f.isIt(from.id))
return false;
return true;
}
public boolean canComment() {
return commands.contains("comment");
}
public boolean canLike() {
return commands.contains("like");
}
public boolean canUnlike() {
return commands.contains("unlike") || hidden;
}
public boolean canHide() {
return commands.contains("hide");// || !hidden;
}
public boolean canUnhide() {
return commands.contains("unhide");
}
public String getToLine() {
if (to.length <= 0 || to.length == 1 && (to[0].id.equals(from.id) || to[0].id.equals(id)))
return null;
List<String> lst = new ArrayList<String>();
for (BaseFeed f : to)
lst.add(f.isMe() ? IdentItem.accountFeed : f.getName());
return TextUtils.join(", ", lst);
}
public String[] getFofIDs() {
if (fof == null || TextUtils.isEmpty(fofHtml))
return null;
Pattern p = Pattern.compile("friendfeed\\.com/((\\d|\\w)+)");
Matcher m = p.matcher(fofHtml);
String f1 = null;
String f2 = null;
if (m.find()) {
f1 = m.group(1);
if (m.find())
f2 = m.group(1);
}
if (f2 != null)
return new String[] { f1, f2 };
if (f1 != null)
return new String[] { f1 };
return null;
}
public int getFilesCount() {
return files.length + thumbnails.length;
}
public int getLikesCount() {
if (likes == null)
return 0;
for (Like l : likes)
if (l.placeholder != null && l.placeholder)
return l.num + likes.size() - 1;
return likes.size();
}
public int getCommentsCount() {
if (comments == null)
return 0;
for (Comment c : comments)
if (c.placeholder != null && c.placeholder)
return c.num + comments.size() - 1;
return comments.size();
}
public String[] getMediaUrls(boolean attachments) {
String[] res = new String[attachments ? thumbnails.length + files.length : thumbnails.length];
for (int i = 0; i < thumbnails.length; i++)
res[i] = thumbnails[i].link;
if (attachments)
for (int i = 0; i < thumbnails.length; i++)
res[thumbnails.length + i] = files[i].url;
return res;
}
public int indexOfComment(String cid) {
Comment c;
for (int i = 0; i < comments.size(); i++) {
c = comments.get(i);
if (!c.placeholder && c.id.equals(cid))
return i;
}
return -1;
}
public int indexOfLike(String userId) {
Like l;
for (int i = 0; i < likes.size(); i++) {
l = likes.get(i);
if (!l.placeholder && l.from.id.equals(userId))
return i;
}
return -1;
}
public boolean hasSpoilers() {
for (Comment c: comments)
if (c.spoiler)
return true;
return false;
}
public boolean hasReplies() {
for (Comment c: comments)
if ((c.created || c.updated) && !c.from.isMe())
return true;
return false;
}
public void thumbNext() {
thumbpos = thumbnails.length > 0 ? (thumbpos + 1) % thumbnails.length : 0;
}
public void thumbPrior() {
thumbpos = thumbnails.length > 0 ? (thumbpos + (thumbnails.length - 1)) % thumbnails.length : 0;
}
public static class Fof {
public String type;
public BaseFeed from;
}
public static class Thumbnail {
public String url = "";
public String link = "";
public int width = 0;
public int height = 0;
public String player = "";
public int rotation = 0; // local
public boolean landscape = true; // local
public String videoId = null; // local
public String videoUrl = null; // local
public boolean isFFMediaPic() {
return link.indexOf("/m.friendfeed-media.com/") > 0;
}
public boolean isSimplePic() {
return link.endsWith(".jpg") || link.endsWith(".jpeg") || link.endsWith(".png") || link.endsWith(".gif");
}
public boolean isYouTube() {
if (TextUtils.isEmpty(player))
return false;
if (!(TextUtils.isEmpty(videoId) || TextUtils.isEmpty(videoUrl)))
return true;
Document doc = Jsoup.parseBodyFragment(player);
Elements emb = doc.getElementsByTag("embed");
if (emb != null && emb.size() > 0 && emb.get(0).hasAttr("src")) {
String src = emb.get(0).attr("src");
if (Commons.YouTube.isVideoUrl(src)) {
videoId = Commons.YouTube.getId(src);
videoUrl = Commons.YouTube.getFriendlyUrl(src);
return true;
}
}
return false;
}
public String videoPreview() {
return isYouTube() ? Commons.YouTube.getPreview(videoUrl) : null;
}
}
public static class Attachment {
public String url;
public String type;
public String name;
public String icon;
public int size = 0;
}
public static class Coordinates {
public String lat;
@SerializedName("long")
public String lon;
}
}
public static class Comment extends BaseEntry {
// compact view only (plus body):
public Boolean placeholder = false;
public int num;
@Override
public void checkLocalHide() {
if (placeholder)
return;
super.checkLocalHide();
if (body.toLowerCase(Locale.getDefault()).equals("sp") ||
body.toLowerCase(Locale.getDefault()).equals("spoiler") ||
rawBody.toLowerCase(Locale.getDefault()).equals("sp") ||
rawBody.toLowerCase(Locale.getDefault()).equals("spoiler"))
spoiler = true;
}
}
public static class Like {
public Date date;
public BaseFeed from;
public boolean created;
public boolean updated;
// compact view only:
public String body;
public Boolean placeholder = false;
public int num;
public boolean isMine() {
return from.isMe();
}
public String getFuzzyTime() {
return DateUtils.getRelativeTimeSpanString(date.getTime(), System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS).toString();
}
}
public static class FeedInfo extends BaseFeed {
public BaseFeed[] feeds = new BaseFeed[] {}; // lists only
public BaseFeed[] subscriptions = new BaseFeed[] {}; // users only
public BaseFeed[] subscribers = new BaseFeed[] {}; // users and groups
public BaseFeed[] admins = new BaseFeed[] {}; // groups only
public BaseFeed findFeedById(String fid) {
for (BaseFeed f: subscriptions)
if (f.isIt(fid))
return f;
for (BaseFeed f: subscribers)
if (f.isIt(fid))
return f;
return null;
}
}
public static class FeedList {
public SectionItem[] main;
public SectionItem[] lists;
public SectionItem[] groups;
public SectionItem[] searches;
public Section[] sections;
public long timestamp = System.currentTimeMillis();
public long getAge() {
return System.currentTimeMillis() - timestamp;
}
public SectionItem getSectionByFeed(String feed_id) {
for (Section s : sections)
for (SectionItem f : s.feeds)
if (f.id.equals(feed_id))
return f;
return null;
}
public static class SectionItem extends BaseFeed {
public String query = "";
}
public static class Section {
public String id;
public String name;
public SectionItem[] feeds;
public boolean hasFeed(String feed_id) {
for (SectionItem si : feeds)
if (si.id.equals(feed_id))
return true;
return false;
}
}
}
public static void dropClients() {
CLIENT_PROFILE = null;
CLIENT_MSGS = null;
CLIENT_FEED = null;
CLIENT_ENTRY = null;
CLIENT_WRITER = null;
}
private static FF CLIENT_PROFILE;
private static FF CLIENT_MSGS;
private static FF CLIENT_FEED;
private static FF CLIENT_ENTRY;
private static FF CLIENT_WRITER;
public static FF client_profile(final FFSession session) {
if (CLIENT_PROFILE == null)
CLIENT_PROFILE = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(
new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
String authText = session.getUsername() + ":" + session.getRemoteKey();
String authData = "Basic " + Base64.encodeToString(authText.getBytes(), 0);
request.addHeader("Authorization", authData);
request.addHeader("User-Agent", Commons.USER_AGENT);
request.addQueryParam("locale", session.getPrefs().getString(PK.LOCALE, "en"));
}
}).setLogLevel(RestAdapter.LogLevel.NONE).setClient(new WaitingUCC()).build().create(FF.class);
return CLIENT_PROFILE;
}
public static FF client_msgs(final FFSession session) {
if (CLIENT_MSGS == null)
CLIENT_MSGS = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(
new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
String authText = session.getUsername() + ":" + session.getRemoteKey();
String authData = "Basic " + Base64.encodeToString(authText.getBytes(), 0);
request.addHeader("Authorization", authData);
request.addHeader("User-Agent", Commons.USER_AGENT);
request.addQueryParam("locale", session.getPrefs().getString(PK.LOCALE, "en"));
request.addQueryParam("maxcomments", "1000");
request.addQueryParam("raw", "1");
}
}).setLogLevel(RestAdapter.LogLevel.NONE).setClient(new WaitingUCC()).build().create(FF.class);
return CLIENT_MSGS;
}
public static FF client_feed(final FFSession session) {
if (CLIENT_FEED == null)
CLIENT_FEED = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(
new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
String authText = session.getUsername() + ":" + session.getRemoteKey();
String authData = "Basic " + Base64.encodeToString(authText.getBytes(), 0);
request.addHeader("Authorization", authData);
request.addHeader("User-Agent", Commons.USER_AGENT);
request.addQueryParam("locale", session.getPrefs().getString(PK.LOCALE, "en"));
request.addQueryParam("maxcomments", "auto");
request.addQueryParam("maxlikes", "auto");
request.addQueryParam("raw", "1");
if (session.getPrefs().getBoolean(PK.FEED_FOF, true))
request.addQueryParam("fof", "1");
if (session.getPrefs().getBoolean(PK.FEED_HID, true))
request.addQueryParam("hidden", "1");
}
}).setLogLevel(RestAdapter.LogLevel.NONE).setClient(new WaitingUCC()).build().create(FF.class);
return CLIENT_FEED;
}
public static FF client_entry(final FFSession session) {
if (CLIENT_ENTRY == null)
CLIENT_ENTRY = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(
new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
String authText = session.getUsername() + ":" + session.getRemoteKey();
String authData = "Basic " + Base64.encodeToString(authText.getBytes(), 0);
request.addHeader("Authorization", authData);
request.addHeader("User-Agent", Commons.USER_AGENT);
request.addQueryParam("locale", session.getPrefs().getString(PK.LOCALE, "en"));
request.addQueryParam("raw", "1");
}
}).setLogLevel(RestAdapter.LogLevel.NONE).setClient(new WaitingUCC()).build().create(FF.class);
return CLIENT_ENTRY;
}
public static FF client_write(final FFSession session) {
if (CLIENT_WRITER == null)
CLIENT_WRITER = new RestAdapter.Builder().setEndpoint(API_URL).setRequestInterceptor(
new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
String authText = session.getUsername() + ":" + session.getRemoteKey();
String authData = "Basic " + Base64.encodeToString(authText.getBytes(), 0);
request.addHeader("Authorization", authData);
request.addQueryParam("appid", API_KEY);
}
}).setLogLevel(RestAdapter.LogLevel.NONE).setClient(new WaitingUCC()).build().create(FF.class);
return CLIENT_WRITER;
}
static class WaitingUCC extends UrlConnectionClient {
@Override
protected HttpURLConnection openConnection(Request request) throws IOException {
HttpURLConnection connection = super.openConnection(request);
connection.setConnectTimeout(20 * 1000); // 20 sec
connection.setReadTimeout(60 * 1000); // 60 sec
return connection;
}
}
}