Post new RSS entries into the user's personal blog

Closes #486
This commit is contained in:
Torsten Grote
2016-07-25 18:58:55 -03:00
parent 8d1a26ba72
commit 6454acdaa5
4 changed files with 185 additions and 64 deletions

View File

@@ -43,6 +43,11 @@ public class Feed {
this(url, blogId, title, description, author, added, 0L, 0L);
}
public Feed(String url, GroupId blogId, long added) {
this(url, blogId, null, null, null, added, 0L, 0L);
}
public String getUrl() {
return url;
}

View File

@@ -1,13 +1,20 @@
package org.briarproject.api.feed;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MINUTES;
public interface FeedConstants {
/* delay after start before fetching feed, in minutes */
/* delay after start before fetching feed */
int FETCH_DELAY_INITIAL = 1;
/* the interval the feed should be fetched, in minutes */
/* the interval the feed should be fetched */
int FETCH_INTERVAL = 30;
/* the unit that applies to the fetch times */
TimeUnit FETCH_UNIT = MINUTES;
// group metadata keys
String KEY_FEEDS = "feeds";
String KEY_FEED_URL = "feedURL";

View File

@@ -191,6 +191,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
@Override
public void removeBlog(Blog b) throws DbException {
// TODO if this gets used, check for RSS feeds posting into this blog
Transaction txn = db.startTransaction(false);
try {
for (RemoveBlogHook hook : removeHooks)

View File

@@ -8,7 +8,10 @@ import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
import org.briarproject.api.FormatException;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPost;
import org.briarproject.api.blogs.BlogPostFactory;
import org.briarproject.api.clients.Client;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.PrivateGroupFactory;
@@ -21,18 +24,27 @@ import org.briarproject.api.db.Transaction;
import org.briarproject.api.feed.Feed;
import org.briarproject.api.feed.FeedManager;
import org.briarproject.api.lifecycle.IoExecutor;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.Service;
import org.briarproject.api.lifecycle.ServiceException;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.system.Clock;
import org.briarproject.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
@@ -44,10 +56,12 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH;
import static org.briarproject.api.feed.FeedConstants.FETCH_DELAY_INITIAL;
import static org.briarproject.api.feed.FeedConstants.FETCH_INTERVAL;
import static org.briarproject.api.feed.FeedConstants.FETCH_UNIT;
import static org.briarproject.api.feed.FeedConstants.KEY_FEEDS;
class FeedManagerImpl implements FeedManager, Service, Client {
@@ -65,19 +79,28 @@ class FeedManagerImpl implements FeedManager, Service, Client {
private final DatabaseComponent db;
private final PrivateGroupFactory privateGroupFactory;
private final ClientHelper clientHelper;
private IdentityManager identityManager;
private final BlogManager blogManager;
@Inject
@SuppressWarnings("WeakerAccess")
BlogPostFactory blogPostFactory;
@Inject
@SuppressWarnings("WeakerAccess")
Clock clock;
@Inject
FeedManagerImpl(ScheduledExecutorService feedExecutor,
@IoExecutor Executor ioExecutor, DatabaseComponent db,
PrivateGroupFactory privateGroupFactory, ClientHelper clientHelper,
BlogManager blogManager) {
IdentityManager identityManager, BlogManager blogManager) {
this.feedExecutor = feedExecutor;
this.ioExecutor = ioExecutor;
this.db = db;
this.privateGroupFactory = privateGroupFactory;
this.clientHelper = clientHelper;
this.identityManager = identityManager;
this.blogManager = blogManager;
}
@@ -99,7 +122,7 @@ class FeedManagerImpl implements FeedManager, Service, Client {
}
};
feedExecutor.scheduleWithFixedDelay(fetcher, FETCH_DELAY_INITIAL,
FETCH_INTERVAL, MINUTES);
FETCH_INTERVAL, FETCH_UNIT);
}
@Override
@@ -124,16 +147,11 @@ class FeedManagerImpl implements FeedManager, Service, Client {
@Override
public void addFeed(String url, GroupId g) throws DbException, IOException {
LOG.info("Adding new RSS feed...");
Feed feed;
// TODO check for existing feed?
Feed feed = new Feed(url, g, clock.currentTimeMillis());
try {
SyndFeed f = getSyndFeed(getFeedInputStream(url));
String title = StringUtils.isNullOrEmpty(f.getTitle()) ? null :
f.getTitle();
String description = f.getDescription();
String author = f.getAuthor();
long added = System.currentTimeMillis();
feed = new Feed(url, g, title, description, author, added);
feed = fetchFeed(feed);
} catch (FeedException e) {
throw new IOException(e);
}
@@ -142,7 +160,7 @@ class FeedManagerImpl implements FeedManager, Service, Client {
try {
List<Feed> feeds = getFeeds(txn);
feeds.add(feed);
storeFeeds(feeds);
storeFeeds(txn, feeds);
} finally {
db.endTransaction(txn);
}
@@ -150,6 +168,7 @@ class FeedManagerImpl implements FeedManager, Service, Client {
@Override
public void removeFeed(String url) throws DbException {
LOG.info("Removing RSS feed...");
Transaction txn = db.startTransaction(false);
try {
List<Feed> feeds = getFeeds(txn);
@@ -222,6 +241,11 @@ class FeedManagerImpl implements FeedManager, Service, Client {
storeFeeds(null, feeds);
}
/**
* This method is called periodically from a background service.
* It fetches all available feeds and posts new entries to the respective
* blog.
*/
private void fetchFeeds() {
LOG.info("Updating RSS feeds...");
@@ -238,7 +262,15 @@ class FeedManagerImpl implements FeedManager, Service, Client {
// Fetch and update all feeds
List<Feed> newFeeds = new ArrayList<Feed>(feeds.size());
for (Feed feed : feeds) {
newFeeds.add(fetchFeed(feed));
try {
newFeeds.add(fetchFeed(feed));
} catch (FeedException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (IOException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
// Store updated feeds
@@ -248,61 +280,45 @@ class FeedManagerImpl implements FeedManager, Service, Client {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
LOG.info("Done updating RSS feeds");
}
private Feed fetchFeed(Feed feed) {
private Feed fetchFeed(Feed feed) throws FeedException, IOException {
if (LOG.isLoggable(INFO))
LOG.info("Fetching feed from " + feed.getUrl());
String title, description, author;
long updated = System.currentTimeMillis();
long updated = clock.currentTimeMillis();
long lastEntryTime = feed.getLastEntryTime();
try {
SyndFeed f = getSyndFeed(getFeedInputStream(feed.getUrl()));
title = f.getTitle();
description = f.getDescription();
author = f.getAuthor();
LOG.info("Title: " + f.getTitle());
LOG.info("Description: " + f.getDescription());
LOG.info("Author: " + f.getAuthor());
LOG.info("Number of Entries: " + f.getEntries().size());
LOG.info("------------------------------");
SyndFeed f = getSyndFeed(getFeedInputStream(feed.getUrl()));
title = StringUtils.isNullOrEmpty(f.getTitle()) ? null : f.getTitle();
if (title != null) title = stripHTML(title);
description = StringUtils.isNullOrEmpty(f.getDescription()) ? null :
f.getDescription();
if (description != null) description = stripHTML(description);
author =
StringUtils.isNullOrEmpty(f.getAuthor()) ? null : f.getAuthor();
if (author != null) author = stripHTML(author);
for (SyndEntry entry : f.getEntries()) {
LOG.info("Entry Title: " + entry.getTitle());
LOG.info("Entry Author: " + entry.getAuthor());
LOG.info("Entry Published Date: " + entry.getPublishedDate());
LOG.info("Entry Updated Date: " + entry.getUpdatedDate());
LOG.info("Entry Link: " + entry.getLink());
LOG.info("Entry URI: " + entry.getUri());
//LOG.info("Entry Description: " + entry.getDescription());
long entryTime;
if (entry.getPublishedDate() != null) {
entryTime = entry.getPublishedDate().getTime();
} else if (entry.getUpdatedDate() != null) {
entryTime = entry.getUpdatedDate().getTime();
} else {
// no time information available, ignore this entry
if (LOG.isLoggable(WARNING))
LOG.warning("Entry has no date: " + entry.getTitle());
continue;
}
if (entryTime > feed.getLastEntryTime()) {
LOG.info("Adding new entry...");
// TODO Pass any new entries down the pipeline to be posted (#486)
for (SyndContent content : entry.getContents()) {
LOG.info("Content: " + content.getValue());
}
if (entryTime > lastEntryTime) lastEntryTime = entryTime;
}
LOG.info("------------------------------");
// sort and add new entries
Collections.sort(f.getEntries(), getEntryComparator());
for (SyndEntry entry : f.getEntries()) {
long entryTime;
if (entry.getPublishedDate() != null) {
entryTime = entry.getPublishedDate().getTime();
} else if (entry.getUpdatedDate() != null) {
entryTime = entry.getUpdatedDate().getTime();
} else {
// no time information available, ignore this entry
if (LOG.isLoggable(WARNING))
LOG.warning("Entry has no date: " + entry.getTitle());
continue;
}
if (entryTime > feed.getLastEntryTime()) {
postEntry(feed, entry);
if (entryTime > lastEntryTime) lastEntryTime = entryTime;
}
} catch (FeedException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return feed;
} catch (IOException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return feed;
}
return new Feed(feed.getUrl(), feed.getBlogId(), title, description,
author, feed.getAdded(), updated, lastEntryTime);
@@ -313,7 +329,7 @@ class FeedManagerImpl implements FeedManager, Service, Client {
// TODO verify and use local Tor proxy address/port
String proxyHost = "localhost";
int proxyPort = 59050;
Proxy proxy = new Proxy(Proxy.Type.HTTP,
Proxy proxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress(proxyHost, proxyPort));
// Build HTTP Client
@@ -338,6 +354,98 @@ class FeedManagerImpl implements FeedManager, Service, Client {
return input.build(new XmlReader(stream));
}
private void postEntry(Feed feed, SyndEntry entry) {
LOG.info("Adding new entry...");
// build post body
StringBuilder b = new StringBuilder();
if (!StringUtils.isNullOrEmpty(entry.getTitle())) {
b.append(stripHTML(entry.getTitle())).append("\n\n");
}
for (SyndContent content : entry.getContents()) {
// extract content and do a very simple HTML tag stripping
if (content.getValue() != null)
b.append(stripHTML(content.getValue()));
}
if (entry.getContents().size() == 0) {
if (entry.getDescription().getValue() != null)
b.append(stripHTML(entry.getDescription().getValue()));
}
if (!StringUtils.isNullOrEmpty(entry.getAuthor())) {
b.append("\n\n-- ").append(stripHTML(entry.getAuthor()));
}
if (entry.getPublishedDate() != null) {
b.append(" (").append(entry.getPublishedDate().toString())
.append(")");
} else if (entry.getUpdatedDate() != null) {
b.append(" (").append(entry.getUpdatedDate().toString())
.append(")");
}
if (!StringUtils.isNullOrEmpty(entry.getLink())) {
b.append("\n\n").append(stripHTML(entry.getLink()));
}
// get other information for post
GroupId groupId = feed.getBlogId();
long time = clock.currentTimeMillis();
byte[] body = getPostBody(b.toString());
try {
// create and store post
Blog blog = blogManager.getBlog(groupId);
AuthorId authorId = blog.getAuthor().getId();
LocalAuthor author = identityManager.getLocalAuthor(authorId);
BlogPost post = blogPostFactory
.createBlogPost(groupId, null, time, null, author,
"text/plain", body);
blogManager.addLocalPost(post);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (GeneralSecurityException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (FormatException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (IllegalArgumentException e) {
// yes even catch this, so we at least get a stacktrace
// and the executor doesn't just die a silent death
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
private String stripHTML(String s) {
return StringUtils.trim(s.replaceAll("<.*?>", ""));
}
private byte[] getPostBody(String text) {
byte[] body = StringUtils.toUtf8(text);
if (body.length <= MAX_BLOG_POST_BODY_LENGTH) return body;
else return Arrays.copyOfRange(body, 0, MAX_BLOG_POST_BODY_LENGTH - 1);
}
/**
* This Comparator assumes that SyndEntry returns a valid Date either for
* getPublishedDate() or getUpdatedDate().
*/
private Comparator<SyndEntry> getEntryComparator() {
return new Comparator<SyndEntry>() {
@Override
public int compare(SyndEntry e1, SyndEntry e2) {
Date d1 =
e1.getPublishedDate() != null ? e1.getPublishedDate() :
e1.getUpdatedDate();
Date d2 =
e2.getPublishedDate() != null ? e2.getPublishedDate() :
e2.getUpdatedDate();
if (d1.after(d2)) return 1;
if (d1.before(d2)) return -1;
return 0;
}
};
}
private Group getLocalGroup() {
return privateGroupFactory.createLocalGroup(getClientId());
}