mirror of
https://github.com/Athou/commafeed.git
synced 2026-11-06 04:16:54 +00:00
use 120 max line length for both server and client code (was 100 and 140)
This commit is contained in:
@@ -34,16 +34,14 @@ public class CommaFeedApplication {
|
||||
System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
|
||||
System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
|
||||
|
||||
PasswordConstraintValidator.setMinimumPasswordLength(
|
||||
config.users().minimumPasswordLength());
|
||||
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||
|
||||
if (config.imageProxyEnabled()) {
|
||||
ImageProxyUrl.generateKey();
|
||||
}
|
||||
|
||||
if (config.passwordRecoveryEnabled() && config.passwordRecoveryPublicBaseUrl().isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"password recovery is enabled but no public base url is set");
|
||||
throw new IllegalStateException("password recovery is enabled but no public base url is set");
|
||||
}
|
||||
|
||||
feedRefreshEngine.start();
|
||||
|
||||
@@ -19,24 +19,20 @@ import java.util.Optional;
|
||||
/**
|
||||
* CommaFeed configuration
|
||||
*
|
||||
* <p>Default values are for production, they can be overridden in application.properties for other
|
||||
* profiles
|
||||
* <p>Default values are for production, they can be overridden in application.properties for other profiles
|
||||
*/
|
||||
@ConfigMapping(prefix = "commafeed")
|
||||
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
|
||||
public interface CommaFeedConfiguration {
|
||||
/**
|
||||
* Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
|
||||
*/
|
||||
/** Whether to expose a robots.txt file that disallows web crawlers and search engine indexers. */
|
||||
@WithDefault("true")
|
||||
boolean hideFromWebCrawlers();
|
||||
|
||||
/**
|
||||
* If enabled, images in feed entries will be proxied through the server instead of accessed
|
||||
* directly by the browser.
|
||||
* If enabled, images in feed entries will be proxied through the server instead of accessed directly by the
|
||||
* browser.
|
||||
*
|
||||
* <p>This is useful if commafeed is accessed through a restricting proxy that blocks some feeds
|
||||
* that are followed.
|
||||
* <p>This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean imageProxyEnabled();
|
||||
@@ -50,9 +46,8 @@ public interface CommaFeedConfiguration {
|
||||
boolean passwordRecoveryEnabled();
|
||||
|
||||
/**
|
||||
* The base URL of the application to use in the password recovery link in the email. We can't
|
||||
* use the URL sent by the browser of the user because of malicious Host header injection
|
||||
* attacks.
|
||||
* The base URL of the application to use in the password recovery link in the email. We can't use the URL sent by
|
||||
* the browser of the user because of malicious Host header injection attacks.
|
||||
*/
|
||||
Optional<String> passwordRecoveryPublicBaseUrl();
|
||||
|
||||
@@ -87,16 +82,14 @@ public interface CommaFeedConfiguration {
|
||||
Websocket websocket();
|
||||
|
||||
/**
|
||||
* Duration to wait for the feed refresh engine and the task scheduler to stop when the
|
||||
* application is shutting down.
|
||||
* Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting
|
||||
* down.
|
||||
*/
|
||||
@WithDefault("2s")
|
||||
Duration shutdownTimeout();
|
||||
|
||||
interface HttpClient {
|
||||
/**
|
||||
* User-Agent string that will be used by the http client, leave empty for the default one.
|
||||
*/
|
||||
/** User-Agent string that will be used by the http client, leave empty for the default one. */
|
||||
Optional<String> userAgent();
|
||||
|
||||
/** Time to wait for a connection to be established. */
|
||||
@@ -123,19 +116,15 @@ public interface CommaFeedConfiguration {
|
||||
@WithDefault("1m")
|
||||
Duration idleConnectionsEvictionInterval();
|
||||
|
||||
/**
|
||||
* If a feed is larger than this, it will be discarded to prevent memory issues while
|
||||
* parsing the feed.
|
||||
*/
|
||||
/** If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed. */
|
||||
@WithDefault("5M")
|
||||
MemorySize maxResponseSize();
|
||||
|
||||
/**
|
||||
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks,
|
||||
* which could potentially expose internal resources.
|
||||
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could
|
||||
* potentially expose internal resources.
|
||||
*
|
||||
* <p>You may want to enable this if you host a public instance of CommaFeed with
|
||||
* registrations open.
|
||||
* <p>You may want to enable this if you host a public instance of CommaFeed with registrations open.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean blockLocalAddresses();
|
||||
@@ -147,9 +136,8 @@ public interface CommaFeedConfiguration {
|
||||
|
||||
interface HttpClientCache {
|
||||
/**
|
||||
* Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts
|
||||
* (e.g. when subscribing to a feed for the first time or when clicking "fetch all my feeds
|
||||
* now").
|
||||
* Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when
|
||||
* subscribing to a feed for the first time or when clicking "fetch all my feeds now").
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean enabled();
|
||||
@@ -169,8 +157,7 @@ public interface CommaFeedConfiguration {
|
||||
Duration interval();
|
||||
|
||||
/**
|
||||
* Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an
|
||||
* upper bound when:
|
||||
* Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an upper bound when:
|
||||
*
|
||||
* <ul>
|
||||
* <li>an error occurs while refreshing a feed and we're backing off exponentially
|
||||
@@ -182,10 +169,10 @@ public interface CommaFeedConfiguration {
|
||||
Duration maxInterval();
|
||||
|
||||
/**
|
||||
* If enabled, CommaFeed will calculate the next refresh time based on the feed's average
|
||||
* time between entries and the time since the last entry was published. The interval will
|
||||
* be sometimes between the default refresh interval (`commafeed.feed-refresh.interval`) and
|
||||
* the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
|
||||
* If enabled, CommaFeed will calculate the next refresh time based on the feed's average time between entries
|
||||
* and the time since the last entry was published. The interval will be sometimes between the default refresh
|
||||
* interval (`commafeed.feed-refresh.interval`) and the maximum refresh interval
|
||||
* (`commafeed.feed-refresh.max-interval`).
|
||||
*
|
||||
* <p>See {@link FeedRefreshIntervalCalculator} for details.
|
||||
*/
|
||||
@@ -207,8 +194,8 @@ public interface CommaFeedConfiguration {
|
||||
int databaseThreads();
|
||||
|
||||
/**
|
||||
* Duration after which a user is considered inactive. Feeds for inactive users are not
|
||||
* refreshed until they log in again.
|
||||
* Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log
|
||||
* in again.
|
||||
*
|
||||
* <p>0 to disable.
|
||||
*/
|
||||
@@ -216,15 +203,15 @@ public interface CommaFeedConfiguration {
|
||||
Duration userInactivityPeriod();
|
||||
|
||||
/**
|
||||
* Duration after which the evaluation of a filtering expresion to mark an entry as read is
|
||||
* considered to have timed out.
|
||||
* Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have
|
||||
* timed out.
|
||||
*/
|
||||
@WithDefault("500ms")
|
||||
Duration filteringExpressionEvaluationTimeout();
|
||||
|
||||
/**
|
||||
* Duration after which the "Fetch all my feeds now" action is available again after use to
|
||||
* avoid spamming feeds.
|
||||
* Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming
|
||||
* feeds.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration forceRefreshCooldownDuration();
|
||||
@@ -240,10 +227,7 @@ public interface CommaFeedConfiguration {
|
||||
@WithDefault("5")
|
||||
int threads();
|
||||
|
||||
/**
|
||||
* Maximum amount of notifications that can be queued before new notifications are
|
||||
* discarded.
|
||||
*/
|
||||
/** Maximum amount of notifications that can be queued before new notifications are discarded. */
|
||||
@Min(1)
|
||||
@WithDefault("100")
|
||||
int queueCapacity();
|
||||
@@ -256,8 +240,8 @@ public interface CommaFeedConfiguration {
|
||||
int retriesBeforeBackoff();
|
||||
|
||||
/**
|
||||
* Duration to wait before retrying after an error. Will be multiplied by the number of
|
||||
* errors since the last successful fetch.
|
||||
* Duration to wait before retrying after an error. Will be multiplied by the number of errors since the last
|
||||
* successful fetch.
|
||||
*/
|
||||
@WithDefault("1h")
|
||||
Duration backoffInterval();
|
||||
@@ -286,8 +270,7 @@ public interface CommaFeedConfiguration {
|
||||
Duration entriesMaxAge();
|
||||
|
||||
/**
|
||||
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will
|
||||
* be deleted.
|
||||
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
|
||||
*
|
||||
* <p>0 to disable.
|
||||
*/
|
||||
@@ -320,9 +303,7 @@ public interface CommaFeedConfiguration {
|
||||
boolean keepStarredEntries();
|
||||
|
||||
default Instant statusesInstantThreshold() {
|
||||
return statusesMaxAge().toMillis() > 0
|
||||
? Instant.now().minus(statusesMaxAge())
|
||||
: null;
|
||||
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,23 +327,17 @@ public interface CommaFeedConfiguration {
|
||||
}
|
||||
|
||||
interface Websocket {
|
||||
/**
|
||||
* Enable websocket connection so the server can notify web clients that there are new
|
||||
* entries for feeds.
|
||||
*/
|
||||
/** Enable websocket connection so the server can notify web clients that there are new entries for feeds. */
|
||||
@WithDefault("true")
|
||||
boolean enabled();
|
||||
|
||||
/**
|
||||
* Interval at which the client will send a ping message on the websocket to keep the
|
||||
* connection alive.
|
||||
*/
|
||||
/** Interval at which the client will send a ping message on the websocket to keep the connection alive. */
|
||||
@WithDefault("15m")
|
||||
Duration pingInterval();
|
||||
|
||||
/**
|
||||
* If the websocket connection is disabled or the connection is lost, the client will reload
|
||||
* the feed tree at this interval.
|
||||
* If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at
|
||||
* this interval.
|
||||
*/
|
||||
@WithDefault("30s")
|
||||
Duration treeReloadInterval();
|
||||
|
||||
@@ -32,11 +32,9 @@ public class ExceptionMappers {
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@ServerExceptionMapper(CommaFeedApplicationException.class)
|
||||
public RestResponse<CommaFeedApplicationError> applicationError(
|
||||
CommaFeedApplicationException e) {
|
||||
public RestResponse<CommaFeedApplicationError> applicationError(CommaFeedApplicationException e) {
|
||||
ResponseBuilder<CommaFeedApplicationError> response =
|
||||
ResponseBuilder.create(
|
||||
e.type().status(), new CommaFeedApplicationError(e.type(), e.getMessage()));
|
||||
ResponseBuilder.create(e.type().status(), new CommaFeedApplicationError(e.type(), e.getMessage()));
|
||||
if (e.type().status() == Status.UNAUTHORIZED) {
|
||||
response.cookie(cookieService.buildLogoutCookie());
|
||||
}
|
||||
@@ -46,8 +44,7 @@ public class ExceptionMappers {
|
||||
@ServerExceptionMapper(UnauthorizedException.class)
|
||||
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
|
||||
return RestResponse.status(
|
||||
Status.UNAUTHORIZED,
|
||||
new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
|
||||
Status.UNAUTHORIZED, new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
|
||||
}
|
||||
|
||||
@ServerExceptionMapper(AuthenticationFailedException.class)
|
||||
|
||||
@@ -56,29 +56,24 @@ public class HttpClientFactory {
|
||||
|
||||
private static final DnsResolver DNS_RESOLVER = SystemDefaultDnsResolver.INSTANCE;
|
||||
private static final IPAddress CGNAT_RANGE = new IPAddressString("100.64.0.0/10").getAddress();
|
||||
private static final IPAddress NAT64_LOCAL_USE_RANGE =
|
||||
new IPAddressString("64:ff9b:1::/48").getAddress();
|
||||
private static final IPAddress NAT64_LOCAL_USE_RANGE = new IPAddressString("64:ff9b:1::/48").getAddress();
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CommaFeedVersion version;
|
||||
|
||||
public CloseableHttpAsyncClient newClient(int poolSize) {
|
||||
PoolingAsyncClientConnectionManager connectionManager =
|
||||
newConnectionManager(config, poolSize);
|
||||
PoolingAsyncClientConnectionManager connectionManager = newConnectionManager(config, poolSize);
|
||||
String userAgent =
|
||||
config.httpClient()
|
||||
.userAgent()
|
||||
.orElseGet(() ->
|
||||
String.format(
|
||||
"CommaFeed/%s (https://github.com/Athou/commafeed)",
|
||||
version.getVersion()));
|
||||
"CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
|
||||
return newClient(config, connectionManager, userAgent);
|
||||
}
|
||||
|
||||
private CloseableHttpAsyncClient newClient(
|
||||
CommaFeedConfiguration config,
|
||||
PoolingAsyncClientConnectionManager connectionManager,
|
||||
String userAgent) {
|
||||
CommaFeedConfiguration config, PoolingAsyncClientConnectionManager connectionManager, String userAgent) {
|
||||
List<Header> headers = new ArrayList<>();
|
||||
headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
|
||||
headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
|
||||
@@ -86,10 +81,8 @@ public class HttpClientFactory {
|
||||
headers.add(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
|
||||
headers.add(new BasicHeader(HttpHeaders.SEC_FETCH_MODE, "navigate"));
|
||||
|
||||
SequencedMap<String, UnaryOperator<AsyncDataConsumer>> contentDecoderMap =
|
||||
new LinkedHashMap<>();
|
||||
contentDecoderMap.put(
|
||||
ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
|
||||
SequencedMap<String, UnaryOperator<AsyncDataConsumer>> contentDecoderMap = new LinkedHashMap<>();
|
||||
contentDecoderMap.put(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
|
||||
contentDecoderMap.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new);
|
||||
|
||||
RedirectStrategy redirectStrategy =
|
||||
@@ -107,8 +100,7 @@ public class HttpClientFactory {
|
||||
.setDefaultHeaders(headers)
|
||||
.setConnectionManager(connectionManager)
|
||||
.evictExpiredConnections()
|
||||
.evictIdleConnections(
|
||||
TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
|
||||
.evictIdleConnections(TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
|
||||
.setContentDecoderMap(new LinkedHashMap<>(contentDecoderMap))
|
||||
.setRedirectStrategy(redirectStrategy)
|
||||
.build();
|
||||
@@ -116,10 +108,8 @@ public class HttpClientFactory {
|
||||
return client;
|
||||
}
|
||||
|
||||
private PoolingAsyncClientConnectionManager newConnectionManager(
|
||||
CommaFeedConfiguration config, int poolSize) {
|
||||
SSLFactory sslFactory =
|
||||
SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
|
||||
private PoolingAsyncClientConnectionManager newConnectionManager(CommaFeedConfiguration config, int poolSize) {
|
||||
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
|
||||
DnsResolver dnsResolver =
|
||||
config.httpClient().blockLocalAddresses()
|
||||
? new BlockLocalAddressesDnsResolver(DNS_RESOLVER)
|
||||
@@ -131,14 +121,12 @@ public class HttpClientFactory {
|
||||
ConnectionConfig.custom()
|
||||
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
|
||||
.setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
|
||||
.setTimeToLive(
|
||||
Timeout.of(config.httpClient().connectionTimeToLive()))
|
||||
.setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
|
||||
.build())
|
||||
.setDefaultTlsConfig(
|
||||
TlsConfig.custom()
|
||||
.setVersionPolicy(HttpVersionPolicy.NEGOTIATE)
|
||||
.setHandshakeTimeout(
|
||||
Timeout.of(config.httpClient().sslHandshakeTimeout()))
|
||||
.setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout()))
|
||||
.build())
|
||||
.setMaxConnPerRoute(poolSize)
|
||||
.setMaxConnTotal(poolSize)
|
||||
@@ -191,8 +179,7 @@ public class HttpClientFactory {
|
||||
InetAddress[] addresses = delegate.resolve(host);
|
||||
for (InetAddress addr : addresses) {
|
||||
if (isLocalAddress(addr)) {
|
||||
throw new UnknownHostException(
|
||||
"Access to local address blocked: " + addr.getHostAddress());
|
||||
throw new UnknownHostException("Access to local address blocked: " + addr.getHostAddress());
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
@@ -228,8 +215,7 @@ public class HttpClientFactory {
|
||||
|
||||
for (InetAddress addr : addresses) {
|
||||
if (isLocalAddress(addr)) {
|
||||
throw new HttpException(
|
||||
"Access to local address blocked: " + addr.getHostAddress());
|
||||
throw new HttpException("Access to local address blocked: " + addr.getHostAddress());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,32 +75,22 @@ public class HttpGetter {
|
||||
this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
|
||||
this.cache = newCache(config);
|
||||
|
||||
metrics.registerGauge(
|
||||
MetricRegistry.name(getClass(), "cache", "size"),
|
||||
() -> cache == null ? 0 : cache.size());
|
||||
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "size"), () -> cache == null ? 0 : cache.size());
|
||||
metrics.registerGauge(
|
||||
MetricRegistry.name(getClass(), "cache", "memoryUsage"),
|
||||
() ->
|
||||
cache == null
|
||||
? 0
|
||||
: cache.asMap().values().stream()
|
||||
.mapToInt(e -> ArrayUtils.getLength(e.content))
|
||||
.sum());
|
||||
: cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum());
|
||||
}
|
||||
|
||||
public HttpResult get(String url)
|
||||
throws IOException,
|
||||
NotModifiedException,
|
||||
TooManyRequestsException,
|
||||
SchemeNotAllowedException {
|
||||
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
|
||||
return get(HttpRequest.builder(url).build());
|
||||
}
|
||||
|
||||
public HttpResult get(HttpRequest request)
|
||||
throws IOException,
|
||||
NotModifiedException,
|
||||
TooManyRequestsException,
|
||||
SchemeNotAllowedException {
|
||||
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
|
||||
URI uri = URI.create(request.getUrl());
|
||||
ensureHttpScheme(uri.getScheme());
|
||||
|
||||
@@ -178,8 +168,7 @@ public class HttpGetter {
|
||||
FutureCallback<Message<org.apache.hc.core5.http.HttpResponse, byte[]>> callback =
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void completed(
|
||||
Message<org.apache.hc.core5.http.HttpResponse, byte[]> result) {}
|
||||
public void completed(Message<org.apache.hc.core5.http.HttpResponse, byte[]> result) {}
|
||||
|
||||
@Override
|
||||
public void failed(Exception ex) {}
|
||||
@@ -283,15 +272,13 @@ public class HttpGetter {
|
||||
}
|
||||
|
||||
return CacheBuilder.newBuilder()
|
||||
.weigher((HttpRequest key, HttpResponse value) ->
|
||||
value.content() != null ? value.content().length : 0)
|
||||
.weigher((HttpRequest key, HttpResponse value) -> value.content() != null ? value.content().length : 0)
|
||||
.maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
|
||||
.expireAfterWrite(cacheConfig.expiration())
|
||||
.build();
|
||||
}
|
||||
|
||||
private record LimitedContentLengthEntityConsumer(
|
||||
@Delegate AsyncEntityConsumer<byte[]> delegate, long maxBytes)
|
||||
private record LimitedContentLengthEntityConsumer(@Delegate AsyncEntityConsumer<byte[]> delegate, long maxBytes)
|
||||
implements AsyncEntityConsumer<byte[]> {
|
||||
|
||||
@Override
|
||||
@@ -324,9 +311,7 @@ public class HttpGetter {
|
||||
@Override
|
||||
protected void data(ByteBuffer src, boolean endOfStream) throws IOException {
|
||||
if (out.size() + src.remaining() > maxBytes) {
|
||||
throw new IOException(
|
||||
"Response size exceeds the maximum allowed size (%s bytes)"
|
||||
.formatted(maxBytes));
|
||||
throw new IOException("Response size exceeds the maximum allowed size (%s bytes)".formatted(maxBytes));
|
||||
}
|
||||
byte[] chunk = new byte[src.remaining()];
|
||||
src.get(chunk);
|
||||
@@ -366,8 +351,7 @@ public class HttpGetter {
|
||||
this(message, null, null);
|
||||
}
|
||||
|
||||
public NotModifiedException(
|
||||
String message, String newLastModifiedHeader, String newEtagHeader) {
|
||||
public NotModifiedException(String message, String newLastModifiedHeader, String newEtagHeader) {
|
||||
super(message);
|
||||
this.newLastModifiedHeader = newLastModifiedHeader;
|
||||
this.newEtagHeader = newEtagHeader;
|
||||
|
||||
@@ -65,11 +65,7 @@ public class Urls {
|
||||
try {
|
||||
return URI.create(baseUrl).resolve(relativeUrl).toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Unable to create absolute url from relative url: {} base: {}",
|
||||
relativeUrl,
|
||||
baseUrl,
|
||||
e);
|
||||
log.debug("Unable to create absolute url from relative url: {} base: {}", relativeUrl, baseUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -86,8 +82,8 @@ public class Urls {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to
|
||||
* identify a feed and avoid duplicates
|
||||
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and
|
||||
* avoid duplicates
|
||||
*/
|
||||
public static String normalize(String url) {
|
||||
if (url == null) {
|
||||
|
||||
@@ -31,9 +31,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
||||
}
|
||||
|
||||
public FeedCategory findById(User user, Long id) {
|
||||
return query().selectFrom(CATEGORY)
|
||||
.where(CATEGORY.user.eq(user), CATEGORY.id.eq(id))
|
||||
.fetchOne();
|
||||
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.id.eq(id)).fetchOne();
|
||||
}
|
||||
|
||||
public FeedCategory findByName(User user, String name, FeedCategory parent) {
|
||||
|
||||
@@ -36,10 +36,7 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
// join on subscriptions to only refresh feeds that have subscribers
|
||||
.join(SUBSCRIPTION)
|
||||
.on(SUBSCRIPTION.feed.eq(FEED))
|
||||
.where(
|
||||
FEED.disabledUntil
|
||||
.isNull()
|
||||
.or(FEED.disabledUntil.lt(Instant.now())));
|
||||
.where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now())));
|
||||
|
||||
if (lastLoginThreshold != null) {
|
||||
query.join(SUBSCRIPTION.user).where(SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold));
|
||||
@@ -53,11 +50,7 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
}
|
||||
|
||||
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
|
||||
return query()
|
||||
.selectFrom(FEED)
|
||||
.where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
|
||||
.fetch()
|
||||
.stream()
|
||||
return query().selectFrom(FEED).where(FEED.normalizedUrlHash.eq(normalizedUrlHash)).fetch().stream()
|
||||
.filter(f -> Strings.CS.equals(normalizedUrl, f.getNormalizedUrl()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
@@ -42,8 +42,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
}
|
||||
|
||||
Set<String> result = new HashSet<>();
|
||||
for (List<String> batch :
|
||||
Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
|
||||
for (List<String> batch : Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
|
||||
result.addAll(
|
||||
query().select(ENTRY.guidHash)
|
||||
.from(ENTRY)
|
||||
@@ -53,8 +52,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(
|
||||
long maxCapacity, long max, boolean keepStarredEntries) {
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max, boolean keepStarredEntries) {
|
||||
NumberExpression<Long> count = ENTRY.id.count();
|
||||
JPAQuery<Tuple> query = query().select(ENTRY.feed.id, count).from(ENTRY);
|
||||
|
||||
@@ -68,8 +66,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
}
|
||||
|
||||
public int delete(Long feedId, long max) {
|
||||
List<FeedEntry> list =
|
||||
query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
|
||||
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
|
||||
return delete(list);
|
||||
}
|
||||
|
||||
@@ -91,10 +88,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
/** Delete the oldest entries of a feed */
|
||||
public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
|
||||
JPAQuery<FeedEntry> query =
|
||||
query().selectFrom(ENTRY)
|
||||
.where(ENTRY.feed.id.eq(feedId))
|
||||
.orderBy(ENTRY.published.asc())
|
||||
.limit(max);
|
||||
query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max);
|
||||
|
||||
if (keepStarredEntries) {
|
||||
query.where(Predicates.isNotStarred(ENTRY));
|
||||
|
||||
@@ -48,9 +48,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public FeedEntryStatusDAO(
|
||||
EntityManager entityManager,
|
||||
FeedEntryTagDAO feedEntryTagDAO,
|
||||
CommaFeedConfiguration config) {
|
||||
EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
|
||||
super(entityManager, FeedEntryStatus.class, config);
|
||||
this.feedEntryTagDAO = feedEntryTagDAO;
|
||||
this.config = config;
|
||||
@@ -58,22 +56,16 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
query().selectFrom(STATUS)
|
||||
.where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub))
|
||||
.fetch();
|
||||
query().selectFrom(STATUS).where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub)).fetch();
|
||||
FeedEntryStatus status = statuses.stream().findFirst().orElse(null);
|
||||
return handleStatus(user, status, sub, entry);
|
||||
}
|
||||
|
||||
/** creates an artificial "unread" status if status is null */
|
||||
private FeedEntryStatus handleStatus(
|
||||
User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
if (status == null) {
|
||||
Instant statusesInstantThreshold =
|
||||
config.database().cleanup().statusesInstantThreshold();
|
||||
boolean read =
|
||||
statusesInstantThreshold != null
|
||||
&& entry.getPublished().isBefore(statusesInstantThreshold);
|
||||
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
|
||||
boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold);
|
||||
status = new FeedEntryStatus(user, sub, entry);
|
||||
status.setRead(read);
|
||||
status.setMarkable(!read);
|
||||
@@ -85,8 +77,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
private void fetchTags(User user, List<FeedEntryStatus> statuses) {
|
||||
Map<Long, List<FeedEntryTag>> tagsByEntryIds =
|
||||
feedEntryTagDAO.findByEntries(
|
||||
user, statuses.stream().map(FeedEntryStatus::getEntry).toList());
|
||||
feedEntryTagDAO.findByEntries(user, statuses.stream().map(FeedEntryStatus::getEntry).toList());
|
||||
for (FeedEntryStatus status : statuses) {
|
||||
List<FeedEntryTag> tags = tagsByEntryIds.get(status.getEntry().getId());
|
||||
status.setTags(tags == null ? List.of() : tags);
|
||||
@@ -258,9 +249,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
Long count = tuple.get(ENTRY.count());
|
||||
Instant published = tuple.get(ENTRY.published.max());
|
||||
for (FeedSubscription sub : subsByFeedId.get(feedId)) {
|
||||
result.put(
|
||||
sub.getId(),
|
||||
new UnreadCount(sub.getId(), count == null ? 0 : count, published));
|
||||
result.put(sub.getId(), new UnreadCount(sub.getId(), count == null ? 0 : count, published));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,8 +292,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.gt(0));
|
||||
|
||||
NumberExpression<Integer> daysDiff =
|
||||
Expressions.numberTemplate(
|
||||
Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published, now);
|
||||
Expressions.numberTemplate(Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published, now);
|
||||
where.and(daysDiff.goe(SUBSCRIPTION.autoMarkAsReadAfterDays));
|
||||
|
||||
where.and(buildUnreadPredicate());
|
||||
@@ -333,11 +321,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
.toList();
|
||||
|
||||
if (!statusIdsToUpdate.isEmpty()) {
|
||||
updated +=
|
||||
updateQuery(STATUS)
|
||||
.where(STATUS.id.in(statusIdsToUpdate))
|
||||
.set(STATUS.read, true)
|
||||
.execute();
|
||||
updated += updateQuery(STATUS).where(STATUS.id.in(statusIdsToUpdate)).set(STATUS.read, true).execute();
|
||||
}
|
||||
|
||||
// Insert new statuses for entries without existing status
|
||||
|
||||
@@ -31,11 +31,7 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
||||
}
|
||||
|
||||
public Map<Long, List<FeedEntryTag>> findByEntries(User user, List<FeedEntry> entries) {
|
||||
return query()
|
||||
.selectFrom(TAG)
|
||||
.where(TAG.user.eq(user), TAG.entry.in(entries))
|
||||
.fetch()
|
||||
.stream()
|
||||
return query().selectFrom(TAG).where(TAG.user.eq(user), TAG.entry.in(entries)).fetch().stream()
|
||||
.collect(Collectors.groupingBy(t -> t.getEntry().getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,7 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||
|
||||
public FeedSubscription findByFeed(User user, Feed feed) {
|
||||
List<FeedSubscription> subs =
|
||||
query().selectFrom(SUBSCRIPTION)
|
||||
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
|
||||
.fetch();
|
||||
query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed)).fetch();
|
||||
FeedSubscription sub = subs.stream().findFirst().orElse(null);
|
||||
return initRelations(sub);
|
||||
}
|
||||
@@ -104,15 +102,11 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||
}
|
||||
|
||||
public Long count(User user) {
|
||||
return query().select(SUBSCRIPTION.count())
|
||||
.from(SUBSCRIPTION)
|
||||
.where(SUBSCRIPTION.user.eq(user))
|
||||
.fetchOne();
|
||||
return query().select(SUBSCRIPTION.count()).from(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)).fetchOne();
|
||||
}
|
||||
|
||||
public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
|
||||
JPQLQuery<FeedSubscription> query =
|
||||
query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
|
||||
JPQLQuery<FeedSubscription> query = query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
|
||||
if (category == null) {
|
||||
query.where(SUBSCRIPTION.category.isNull());
|
||||
} else {
|
||||
@@ -122,11 +116,9 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||
}
|
||||
|
||||
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
|
||||
Set<Long> categoryIds =
|
||||
categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
|
||||
Set<Long> categoryIds = categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
|
||||
return findAll(user).stream()
|
||||
.filter(s ->
|
||||
s.getCategory() != null && categoryIds.contains(s.getCategory().getId()))
|
||||
.filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,7 @@ public class FacebookFaviconFetcher implements FaviconFetcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
String iconUrl =
|
||||
String.format(
|
||||
"https://graph.facebook.com/%s/picture?type=square&height=16", userName);
|
||||
String iconUrl = String.format("https://graph.facebook.com/%s/picture?type=square&height=16", userName);
|
||||
|
||||
try {
|
||||
log.debug("Getting Facebook user's icon, {}", url);
|
||||
|
||||
@@ -17,8 +17,7 @@ public record Favicon(byte[] icon, MediaType mediaType) {
|
||||
try {
|
||||
return MediaType.valueOf(contentType);
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"invalid content type '{}' received, returning default value", contentType, e);
|
||||
log.debug("invalid content type '{}' received, returning default value", contentType, e);
|
||||
return DEFAULT_MEDIA_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,7 @@ public class RootFaviconFetcher implements FaviconFetcher {
|
||||
URI uri = URI.create(url.trim());
|
||||
String faviconUrl =
|
||||
"%s://%s%s/favicon.ico"
|
||||
.formatted(
|
||||
uri.getScheme(),
|
||||
uri.getHost(),
|
||||
uri.getPort() > 0 ? ":" + uri.getPort() : "");
|
||||
.formatted(uri.getScheme(), uri.getHost(), uri.getPort() > 0 ? ":" + uri.getPort() : "");
|
||||
|
||||
log.debug("getting root icon at {}", faviconUrl);
|
||||
HttpResult result = getter.get(faviconUrl);
|
||||
|
||||
@@ -37,8 +37,7 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
|
||||
|
||||
private static final JsonPointer CHANNEL_THUMBNAIL_URL =
|
||||
JsonPointer.compile("/items/0/snippet/thumbnails/default/url");
|
||||
private static final JsonPointer PLAYLIST_CHANNEL_ID =
|
||||
JsonPointer.compile("/items/0/snippet/channelId");
|
||||
private static final JsonPointer PLAYLIST_CHANNEL_ID = JsonPointer.compile("/items/0/snippet/channelId");
|
||||
|
||||
private final HttpGetter getter;
|
||||
private final CommaFeedConfiguration config;
|
||||
@@ -60,17 +59,11 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
|
||||
try {
|
||||
List<NameValuePair> params = new URIBuilder(url).getQueryParams();
|
||||
Optional<NameValuePair> userId =
|
||||
params.stream()
|
||||
.filter(nvp -> nvp.getName().equalsIgnoreCase("user"))
|
||||
.findFirst();
|
||||
params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst();
|
||||
Optional<NameValuePair> channelId =
|
||||
params.stream()
|
||||
.filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id"))
|
||||
.findFirst();
|
||||
params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst();
|
||||
Optional<NameValuePair> playlistId =
|
||||
params.stream()
|
||||
.filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id"))
|
||||
.findFirst();
|
||||
params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id")).findFirst();
|
||||
|
||||
byte[] response = null;
|
||||
if (userId.isPresent()) {
|
||||
@@ -103,10 +96,7 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
|
||||
}
|
||||
|
||||
private byte[] fetchForUser(String googleAuthKey, String userId)
|
||||
throws IOException,
|
||||
NotModifiedException,
|
||||
TooManyRequestsException,
|
||||
SchemeNotAllowedException {
|
||||
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
|
||||
URI uri =
|
||||
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
||||
.queryParam("part", PART_SNIPPET)
|
||||
@@ -117,10 +107,7 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
|
||||
}
|
||||
|
||||
private byte[] fetchForChannel(String googleAuthKey, String channelId)
|
||||
throws IOException,
|
||||
NotModifiedException,
|
||||
TooManyRequestsException,
|
||||
SchemeNotAllowedException {
|
||||
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
|
||||
URI uri =
|
||||
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
|
||||
.queryParam("part", PART_SNIPPET)
|
||||
@@ -131,10 +118,7 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
|
||||
}
|
||||
|
||||
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
|
||||
throws IOException,
|
||||
NotModifiedException,
|
||||
TooManyRequestsException,
|
||||
SchemeNotAllowedException {
|
||||
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
|
||||
URI uri =
|
||||
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
|
||||
.queryParam("part", PART_SNIPPET)
|
||||
|
||||
@@ -36,8 +36,7 @@ public class FeedFetcher {
|
||||
private final HttpGetter getter;
|
||||
private final List<FeedURLProvider> urlProviders;
|
||||
|
||||
public FeedFetcher(
|
||||
FeedParser parser, HttpGetter getter, @All List<FeedURLProvider> urlProviders) {
|
||||
public FeedFetcher(FeedParser parser, HttpGetter getter, @All List<FeedURLProvider> urlProviders) {
|
||||
this.parser = parser;
|
||||
this.getter = getter;
|
||||
this.urlProviders = urlProviders;
|
||||
@@ -58,9 +57,7 @@ public class FeedFetcher {
|
||||
NoFeedFoundException {
|
||||
log.debug("Fetching feed {}", feedUrl);
|
||||
|
||||
HttpResult result =
|
||||
getter.get(
|
||||
HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
|
||||
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
|
||||
byte[] content = result.content();
|
||||
|
||||
FeedParserResult parserResult;
|
||||
@@ -69,19 +66,12 @@ public class FeedFetcher {
|
||||
} catch (FeedParsingException e) {
|
||||
if (extractFeedUrlFromHtml) {
|
||||
String extractedUrl =
|
||||
extractFeedUrl(
|
||||
urlProviders,
|
||||
feedUrl,
|
||||
new String(result.content(), StandardCharsets.UTF_8));
|
||||
extractFeedUrl(urlProviders, feedUrl, new String(result.content(), StandardCharsets.UTF_8));
|
||||
if (StringUtils.isNotBlank(extractedUrl)) {
|
||||
feedUrl = extractedUrl;
|
||||
|
||||
result =
|
||||
getter.get(
|
||||
HttpRequest.builder(extractedUrl)
|
||||
.lastModified(lastModified)
|
||||
.eTag(eTag)
|
||||
.build());
|
||||
getter.get(HttpRequest.builder(extractedUrl).lastModified(lastModified).eTag(eTag).build());
|
||||
content = result.content();
|
||||
parserResult = parser.parse(result.urlAfterRedirect(), content);
|
||||
} else {
|
||||
@@ -96,8 +86,7 @@ public class FeedFetcher {
|
||||
throw new IOException("Feed content is empty.");
|
||||
}
|
||||
|
||||
boolean lastModifiedHeaderValueChanged =
|
||||
!Strings.CS.equals(lastModified, result.lastModifiedSince());
|
||||
boolean lastModifiedHeaderValueChanged = !Strings.CS.equals(lastModified, result.lastModifiedSince());
|
||||
boolean etagHeaderValueChanged = !Strings.CS.equals(eTag, result.eTag());
|
||||
|
||||
String hash = Digests.sha1Hex(content);
|
||||
@@ -109,8 +98,7 @@ public class FeedFetcher {
|
||||
etagHeaderValueChanged ? result.eTag() : null);
|
||||
}
|
||||
|
||||
if (lastPublishedDate != null
|
||||
&& lastPublishedDate.equals(parserResult.lastPublishedDate())) {
|
||||
if (lastPublishedDate != null && lastPublishedDate.equals(parserResult.lastPublishedDate())) {
|
||||
log.debug("publishedDate not modified: {}", feedUrl);
|
||||
throw new NotModifiedException(
|
||||
"publishedDate not modified",
|
||||
@@ -127,8 +115,7 @@ public class FeedFetcher {
|
||||
result.validFor());
|
||||
}
|
||||
|
||||
private static String extractFeedUrl(
|
||||
List<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||
private static String extractFeedUrl(List<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||
return urlProviders.stream()
|
||||
.flatMap(provider -> provider.get(url, urlContent).stream())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
|
||||
@@ -67,8 +67,7 @@ public class FeedRefreshEngine {
|
||||
|
||||
this.queue = new LinkedBlockingDeque<>();
|
||||
|
||||
metrics.register(
|
||||
MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||
metrics.register(
|
||||
MetricRegistry.name(getClass(), "worker", "active"),
|
||||
(Gauge<Integer>) () -> workerExecutor.getActiveCount());
|
||||
@@ -88,12 +87,10 @@ public class FeedRefreshEngine {
|
||||
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
|
||||
this.refillExecutor = newDiscardingSingleThreadExecutorService();
|
||||
this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
|
||||
this.databaseUpdaterExecutor =
|
||||
newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
||||
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
||||
this.notifierExecutor =
|
||||
newDiscardingExecutorService(
|
||||
config.pushNotifications().threads(),
|
||||
config.pushNotifications().queueCapacity());
|
||||
config.pushNotifications().threads(), config.pushNotifications().queueCapacity());
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -169,9 +166,7 @@ public class FeedRefreshEngine {
|
||||
refill.mark();
|
||||
|
||||
List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
|
||||
log.debug(
|
||||
"found {} feeds that are up for refresh",
|
||||
nextUpdatableFeeds.size());
|
||||
log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size());
|
||||
for (Feed feed : nextUpdatableFeeds) {
|
||||
// add the feed only if it was not already queued
|
||||
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
|
||||
@@ -199,8 +194,7 @@ public class FeedRefreshEngine {
|
||||
|
||||
notifier.notifyOverWebsocket(sub, entries);
|
||||
return CompletableFuture.runAsync(
|
||||
() -> notifier.sendPushNotifications(sub, entries),
|
||||
notifierExecutor);
|
||||
() -> notifier.sendPushNotifications(sub, entries), notifierExecutor);
|
||||
})
|
||||
.toList();
|
||||
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
|
||||
@@ -222,8 +216,7 @@ public class FeedRefreshEngine {
|
||||
// update disabledUntil to prevent feeds from being returned again by
|
||||
// feedDAO.findNextUpdatable()
|
||||
Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval());
|
||||
feedDAO.setDisabledUntil(
|
||||
feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
||||
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
||||
}
|
||||
return feeds;
|
||||
});
|
||||
@@ -234,26 +227,19 @@ public class FeedRefreshEngine {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
MoreExecutors.shutdownAndAwaitTermination(
|
||||
this.feedProcessingLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(
|
||||
this.refillLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(
|
||||
this.databaseUpdaterExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
|
||||
MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
|
||||
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an ExecutorService with a single thread that discards tasks if a task is already
|
||||
* running
|
||||
*/
|
||||
/** returns an ExecutorService with a single thread that discards tasks if a task is already running */
|
||||
private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
|
||||
ThreadPoolExecutor pool =
|
||||
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
||||
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
|
||||
return pool;
|
||||
}
|
||||
@@ -262,11 +248,7 @@ public class FeedRefreshEngine {
|
||||
private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) {
|
||||
ThreadPoolExecutor pool =
|
||||
new ThreadPoolExecutor(
|
||||
threads,
|
||||
threads,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(queueCapacity));
|
||||
threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(queueCapacity));
|
||||
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
|
||||
return pool;
|
||||
}
|
||||
@@ -274,8 +256,7 @@ public class FeedRefreshEngine {
|
||||
/** returns an ExecutorService that blocks submissions until a thread is available */
|
||||
private ThreadPoolExecutor newBlockingExecutorService(int threads) {
|
||||
ThreadPoolExecutor pool =
|
||||
new ThreadPoolExecutor(
|
||||
threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
||||
new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
||||
pool.setRejectedExecutionHandler((r, e) -> {
|
||||
if (e.isShutdown()) {
|
||||
return;
|
||||
|
||||
@@ -22,8 +22,7 @@ public class FeedRefreshIntervalCalculator {
|
||||
private final FeedRefreshErrorHandling errorHandling;
|
||||
private final InstantSource instantSource;
|
||||
|
||||
public FeedRefreshIntervalCalculator(
|
||||
CommaFeedConfiguration config, InstantSource instantSource) {
|
||||
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config, InstantSource instantSource) {
|
||||
this.interval = config.feedRefresh().interval();
|
||||
this.maxInterval = config.feedRefresh().maxInterval();
|
||||
this.empirical = config.feedRefresh().intervalEmpirical();
|
||||
@@ -31,8 +30,7 @@ public class FeedRefreshIntervalCalculator {
|
||||
this.instantSource = instantSource;
|
||||
}
|
||||
|
||||
public Instant onFetchSuccess(
|
||||
Instant publishedDate, Long averageEntryInterval, Duration validFor) {
|
||||
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval, Duration validFor) {
|
||||
Instant instant =
|
||||
empirical
|
||||
? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
|
||||
@@ -54,14 +52,11 @@ public class FeedRefreshIntervalCalculator {
|
||||
}
|
||||
|
||||
Duration retryInterval =
|
||||
errorHandling
|
||||
.backoffInterval()
|
||||
.multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
|
||||
errorHandling.backoffInterval().multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
|
||||
return constrainToBounds(instantSource.instant().plus(retryInterval));
|
||||
}
|
||||
|
||||
private Instant computeEmpiricalRefreshInterval(
|
||||
Instant publishedDate, Long averageEntryInterval) {
|
||||
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval) {
|
||||
Instant now = instantSource.instant();
|
||||
|
||||
if (publishedDate == null) {
|
||||
@@ -80,9 +75,7 @@ public class FeedRefreshIntervalCalculator {
|
||||
int factor = 2;
|
||||
long millis =
|
||||
Longs.constrainToRange(
|
||||
averageEntryInterval / factor,
|
||||
interval.toMillis(),
|
||||
maxInterval.dividedBy(4).toMillis());
|
||||
averageEntryInterval / factor, interval.toMillis(), maxInterval.dividedBy(4).toMillis());
|
||||
return now.plusMillis(millis);
|
||||
} else {
|
||||
// unknown case
|
||||
|
||||
@@ -64,8 +64,7 @@ public class FeedRefreshUpdater {
|
||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||
}
|
||||
|
||||
private AddEntryResult addEntry(
|
||||
final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
|
||||
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
|
||||
boolean processed = false;
|
||||
FeedEntry insertedEntry = null;
|
||||
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>();
|
||||
@@ -111,11 +110,7 @@ public class FeedRefreshUpdater {
|
||||
log.error("lock timeout for {} - {}", feed.getUrl(), key1);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.error(
|
||||
"interrupted while waiting for lock for {} : {}",
|
||||
feed.getUrl(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
log.error("interrupted while waiting for lock for {} : {}", feed.getUrl(), e.getMessage(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (locked1) {
|
||||
@@ -131,13 +126,11 @@ public class FeedRefreshUpdater {
|
||||
public FeedRefreshUpdaterResult update(Feed feed, List<Entry> entries) {
|
||||
boolean processed = true;
|
||||
long inserted = 0;
|
||||
Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription =
|
||||
new HashMap<>();
|
||||
Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription = new HashMap<>();
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
List<FeedSubscription> subscriptions = null;
|
||||
List<Entry> newEntries =
|
||||
unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries));
|
||||
List<Entry> newEntries = unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries));
|
||||
for (Entry entry : newEntries) {
|
||||
if (subscriptions == null) {
|
||||
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
|
||||
@@ -176,9 +169,7 @@ public class FeedRefreshUpdater {
|
||||
}
|
||||
|
||||
private record AddEntryResult(
|
||||
boolean processed,
|
||||
FeedEntry insertedEntry,
|
||||
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {}
|
||||
boolean processed, FeedEntry insertedEntry, Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {}
|
||||
|
||||
public record FeedRefreshUpdaterResult(
|
||||
Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription) {}
|
||||
|
||||
@@ -22,8 +22,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link
|
||||
* FeedRefreshUpdater} does that)
|
||||
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link FeedRefreshUpdater}
|
||||
* does that)
|
||||
*/
|
||||
@Slf4j
|
||||
@Singleton
|
||||
@@ -68,10 +68,7 @@ public class FeedRefreshWorker {
|
||||
Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
|
||||
if (!entriesMaxAge.isZero()) {
|
||||
Instant threshold = Instant.now().minus(entriesMaxAge);
|
||||
entries =
|
||||
entries.stream()
|
||||
.filter(entry -> entry.published().isAfter(threshold))
|
||||
.toList();
|
||||
entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList();
|
||||
}
|
||||
|
||||
String urlAfterRedirect = result.urlAfterRedirect();
|
||||
@@ -121,9 +118,7 @@ public class FeedRefreshWorker {
|
||||
|
||||
feed.setErrorCount(feed.getErrorCount() + 1);
|
||||
feed.setMessage("Server indicated that we are sending too many requests");
|
||||
feed.setDisabledUntil(
|
||||
refreshIntervalCalculator.onTooManyRequests(
|
||||
e.getRetryAfter(), feed.getErrorCount()));
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onTooManyRequests(e.getRetryAfter(), feed.getErrorCount()));
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -30,15 +30,12 @@ public class FeedUpdateNotifier {
|
||||
|
||||
public void notifyOverWebsocket(FeedSubscription sub, List<FeedEntry> entries) {
|
||||
if (!entries.isEmpty()) {
|
||||
webSocketSessions.sendMessage(
|
||||
sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
|
||||
webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPushNotifications(FeedSubscription sub, List<FeedEntry> entries) {
|
||||
if (!config.pushNotifications().enabled()
|
||||
|| !sub.isPushNotificationsEnabled()
|
||||
|| entries.isEmpty()) {
|
||||
if (!config.pushNotifications().enabled() || !sub.isPushNotificationsEnabled() || entries.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ public class FeedUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TextDirectionDetector.detect(stripped)
|
||||
== TextDirectionDetector.Direction.RIGHT_TO_LEFT;
|
||||
return TextDirectionDetector.detect(stripped) == TextDirectionDetector.Direction.RIGHT_TO_LEFT;
|
||||
}
|
||||
|
||||
public static String getFaviconUrl(FeedSubscription subscription) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.nio.charset.Charset;
|
||||
public class EncodingDetector {
|
||||
|
||||
/**
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by
|
||||
* detecting the characters used in the feed
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the
|
||||
* characters used in the feed
|
||||
*/
|
||||
public Charset getEncoding(byte[] bytes) {
|
||||
String extracted = extractDeclaredEncoding(bytes);
|
||||
|
||||
@@ -42,14 +42,12 @@ import java.util.stream.Collectors;
|
||||
@Singleton
|
||||
public class FeedParser {
|
||||
|
||||
private static final Namespace ATOM_10_NS =
|
||||
Namespace.getNamespace("http://www.w3.org/2005/Atom");
|
||||
private static final Namespace ATOM_10_NS = Namespace.getNamespace("http://www.w3.org/2005/Atom");
|
||||
|
||||
private static final Instant START = Instant.ofEpochMilli(86400000);
|
||||
private static final Instant END = Instant.ofEpochMilli(1000L * Integer.MAX_VALUE - 86400000);
|
||||
|
||||
private static final Comparator<Entry> ENTRY_COMPARATOR =
|
||||
Comparator.comparing(Entry::published).reversed();
|
||||
private static final Comparator<Entry> ENTRY_COMPARATOR = Comparator.comparing(Entry::published).reversed();
|
||||
|
||||
private final EncodingDetector encodingDetector;
|
||||
private final XMLCleaner xmlCleaner;
|
||||
@@ -78,20 +76,13 @@ public class FeedParser {
|
||||
List<Entry> entries = buildEntries(feed, feedUrl);
|
||||
Instant lastEntryDate = entries.stream().findFirst().map(Entry::published).orElse(null);
|
||||
Instant lastPublishedDate = toValidInstant(feed.getPublishedDate(), false);
|
||||
if (lastPublishedDate == null
|
||||
|| lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) {
|
||||
if (lastPublishedDate == null || lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) {
|
||||
lastPublishedDate = lastEntryDate;
|
||||
}
|
||||
Long averageEntryInterval = averageTimeBetweenEntries(entries);
|
||||
|
||||
return new FeedParserResult(
|
||||
title,
|
||||
link,
|
||||
iconUrl,
|
||||
lastPublishedDate,
|
||||
averageEntryInterval,
|
||||
lastEntryDate,
|
||||
entries);
|
||||
title, link, iconUrl, lastPublishedDate, averageEntryInterval, lastEntryDate, entries);
|
||||
} catch (FeedParsingException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -151,9 +142,7 @@ public class FeedParser {
|
||||
String author = StringUtils.trimToNull(item.getAuthor());
|
||||
String categories =
|
||||
StringUtils.trimToNull(
|
||||
item.getCategories().stream()
|
||||
.map(SyndCategory::getName)
|
||||
.collect(Collectors.joining(", ")));
|
||||
item.getCategories().stream().map(SyndCategory::getName).collect(Collectors.joining(", ")));
|
||||
|
||||
Enclosure enclosure = buildEnclosure(item);
|
||||
Media media = buildMedia(item);
|
||||
@@ -281,9 +270,7 @@ public class FeedParser {
|
||||
SummaryStatistics stats = new SummaryStatistics();
|
||||
for (int i = 0; i < entries.size() - 1; i++) {
|
||||
long diff =
|
||||
Math.abs(
|
||||
entries.get(i).published().toEpochMilli()
|
||||
- entries.get(i + 1).published().toEpochMilli());
|
||||
Math.abs(entries.get(i).published().toEpochMilli() - entries.get(i + 1).published().toEpochMilli());
|
||||
stats.addValue(diff);
|
||||
}
|
||||
return (long) stats.getMean();
|
||||
|
||||
@@ -14,18 +14,9 @@ public record FeedParserResult(
|
||||
public record Entry(String guid, String url, Instant published, Content content) {}
|
||||
|
||||
public record Content(
|
||||
String title,
|
||||
String content,
|
||||
String author,
|
||||
String categories,
|
||||
Enclosure enclosure,
|
||||
Media media) {}
|
||||
String title, String content, String author, String categories, Enclosure enclosure, Media media) {}
|
||||
|
||||
public record Enclosure(String url, String type) {}
|
||||
|
||||
public record Media(
|
||||
String description,
|
||||
String thumbnailUrl,
|
||||
Integer thumbnailWidth,
|
||||
Integer thumbnailHeight) {}
|
||||
public record Media(String description, String thumbnailUrl, Integer thumbnailWidth, Integer thumbnailHeight) {}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ import java.util.regex.Pattern;
|
||||
@Singleton
|
||||
public class XMLCleaner {
|
||||
|
||||
private static final Pattern DOCTYPE_PATTERN =
|
||||
Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final Trie trie =
|
||||
Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build();
|
||||
private final Trie trie = Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build();
|
||||
|
||||
public String clean(String xml) {
|
||||
xml = removeCharactersBeforeFirstXmlTag(xml);
|
||||
|
||||
@@ -155,8 +155,7 @@ public class UserSettings extends AbstractModel {
|
||||
private boolean instapaper;
|
||||
private boolean buffer;
|
||||
|
||||
@Embedded
|
||||
private PushNotificationUserSettings pushNotifications = new PushNotificationUserSettings();
|
||||
@Embedded private PushNotificationUserSettings pushNotifications = new PushNotificationUserSettings();
|
||||
|
||||
@Embeddable
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
@@ -49,8 +49,7 @@ public class OPMLExporter {
|
||||
}
|
||||
|
||||
// export root subscriptions
|
||||
for (FeedSubscription sub :
|
||||
subscriptions.stream().filter(s -> s.getCategory() == null).toList()) {
|
||||
for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).toList()) {
|
||||
opml.getOutlines().add(buildSubscriptionOutline(sub));
|
||||
}
|
||||
|
||||
@@ -65,17 +64,14 @@ public class OPMLExporter {
|
||||
|
||||
for (FeedCategory child :
|
||||
categories.stream()
|
||||
.filter(c ->
|
||||
c.getParent() != null && c.getParent().getId().equals(cat.getId()))
|
||||
.filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId()))
|
||||
.toList()) {
|
||||
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
|
||||
}
|
||||
|
||||
for (FeedSubscription sub :
|
||||
subscriptions.stream()
|
||||
.filter(s ->
|
||||
s.getCategory() != null
|
||||
&& s.getCategory().getId().equals(cat.getId()))
|
||||
.filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId()))
|
||||
.toList()) {
|
||||
outline.getChildren().add(buildSubscriptionOutline(sub));
|
||||
}
|
||||
|
||||
@@ -79,8 +79,7 @@ public class OPMLImporter {
|
||||
}
|
||||
// make sure we continue with the import process even if a feed failed
|
||||
try {
|
||||
feedSubscriptionService.subscribe(
|
||||
user, outline.getXmlUrl(), name, parent, position);
|
||||
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
|
||||
} catch (Exception e) {
|
||||
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
|
||||
}
|
||||
|
||||
@@ -33,10 +33,8 @@ import java.util.List;
|
||||
public class FeedEntryContentCleaningService {
|
||||
|
||||
private static final Safelist HTML_WHITELIST = buildWhiteList();
|
||||
private static final List<String> ALLOWED_IFRAME_CSS_RULES =
|
||||
Arrays.asList("height", "width", "border");
|
||||
private static final List<String> ALLOWED_IMG_CSS_RULES =
|
||||
Arrays.asList("display", "width", "height");
|
||||
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
|
||||
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
|
||||
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] {'(', ')'};
|
||||
|
||||
public String clean(String content, String baseUri, boolean keepTextOnly) {
|
||||
@@ -59,8 +57,7 @@ public class FeedEntryContentCleaningService {
|
||||
e.attr("style", escaped);
|
||||
}
|
||||
|
||||
clean.outputSettings(
|
||||
new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
|
||||
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
|
||||
Element body = clean.body();
|
||||
if (keepTextOnly) {
|
||||
content = body.text();
|
||||
@@ -126,24 +123,13 @@ public class FeedEntryContentCleaningService {
|
||||
whitelist.addAttributes("blockquote", "cite");
|
||||
whitelist.addAttributes("col", "span", "width");
|
||||
whitelist.addAttributes("colgroup", "span", "width");
|
||||
whitelist.addAttributes(
|
||||
"iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
|
||||
whitelist.addAttributes("ol", "start", "type");
|
||||
whitelist.addAttributes("q", "cite");
|
||||
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
|
||||
whitelist.addAttributes(
|
||||
"td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes(
|
||||
"th",
|
||||
"border",
|
||||
"bordercolor",
|
||||
"abbr",
|
||||
"axis",
|
||||
"colspan",
|
||||
"rowspan",
|
||||
"scope",
|
||||
"width");
|
||||
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
|
||||
whitelist.addAttributes("ul", "type");
|
||||
|
||||
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
|
||||
@@ -161,8 +147,7 @@ public class FeedEntryContentCleaningService {
|
||||
String rule = "";
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl =
|
||||
buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
@@ -187,8 +172,7 @@ public class FeedEntryContentCleaningService {
|
||||
String rule = "";
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl =
|
||||
buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
@@ -216,8 +200,7 @@ public class FeedEntryContentCleaningService {
|
||||
new ErrorHandler() {
|
||||
@Override
|
||||
public void warning(CSSParseException exception) throws CSSException {
|
||||
log.debug(
|
||||
"warning while parsing css: {}", exception.getMessage(), exception);
|
||||
log.debug("warning while parsing css: {}", exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -227,10 +210,7 @@ public class FeedEntryContentCleaningService {
|
||||
|
||||
@Override
|
||||
public void fatalError(CSSParseException exception) throws CSSException {
|
||||
log.debug(
|
||||
"fatal error while parsing css: {}",
|
||||
exception.getMessage(),
|
||||
exception);
|
||||
log.debug("fatal error while parsing css: {}", exception.getMessage(), exception);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ public class FeedEntryContentService {
|
||||
public FeedEntryContent findOrCreate(Content content, String baseUrl) {
|
||||
FeedEntryContent entryContent = buildContent(content, baseUrl);
|
||||
Optional<FeedEntryContent> existing =
|
||||
feedEntryContentDAO
|
||||
.findExisting(entryContent.getContentHash(), entryContent.getTitleHash())
|
||||
.stream()
|
||||
feedEntryContentDAO.findExisting(entryContent.getContentHash(), entryContent.getTitleHash()).stream()
|
||||
.filter(entryContent::equivalentTo)
|
||||
.findFirst();
|
||||
if (existing.isPresent()) {
|
||||
@@ -44,11 +42,9 @@ public class FeedEntryContentService {
|
||||
FeedEntryContent entryContent = new FeedEntryContent();
|
||||
entryContent.setTitleHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.title())));
|
||||
entryContent.setContentHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.content())));
|
||||
entryContent.setTitle(
|
||||
FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048));
|
||||
entryContent.setTitle(FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048));
|
||||
entryContent.setContent(cleaningService.clean(content.content(), baseUrl, false));
|
||||
entryContent.setAuthor(
|
||||
FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128));
|
||||
entryContent.setAuthor(FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128));
|
||||
entryContent.setCategories(FeedUtils.truncate(content.categories(), 4096));
|
||||
entryContent.setDirection(
|
||||
FeedUtils.isRTL(content.title(), content.content())
|
||||
@@ -63,8 +59,7 @@ public class FeedEntryContentService {
|
||||
|
||||
Media media = content.media();
|
||||
if (media != null) {
|
||||
entryContent.setMediaDescription(
|
||||
cleaningService.clean(media.description(), baseUrl, false));
|
||||
entryContent.setMediaDescription(cleaningService.clean(media.description(), baseUrl, false));
|
||||
entryContent.setMediaThumbnailUrl(FeedUtils.truncate(media.thumbnailUrl(), 2048));
|
||||
entryContent.setMediaThumbnailWidth(media.thumbnailWidth());
|
||||
entryContent.setMediaThumbnailHeight(media.thumbnailHeight());
|
||||
|
||||
@@ -50,27 +50,17 @@ public class FeedEntryFilteringService {
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public boolean filterMatchesEntry(String filter, FeedEntry entry)
|
||||
throws FeedEntryFilterException {
|
||||
public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException {
|
||||
if (StringUtils.isBlank(filter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String title =
|
||||
entry.getContent().getTitle() == null
|
||||
? ""
|
||||
: Jsoup.parse(entry.getContent().getTitle()).text();
|
||||
String author =
|
||||
entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor();
|
||||
String title = entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text();
|
||||
String author = entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor();
|
||||
String content =
|
||||
entry.getContent().getContent() == null
|
||||
? ""
|
||||
: Jsoup.parse(entry.getContent().getContent()).text();
|
||||
entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text();
|
||||
String url = entry.getUrl() == null ? "" : entry.getUrl();
|
||||
String categories =
|
||||
entry.getContent().getCategories() == null
|
||||
? ""
|
||||
: entry.getContent().getCategories();
|
||||
String categories = entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", title);
|
||||
@@ -97,11 +87,9 @@ public class FeedEntryFilteringService {
|
||||
TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FeedEntryFilterException(
|
||||
"interrupted while evaluating expression " + filter, e);
|
||||
throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new FeedEntryFilterException(
|
||||
"Exception while evaluating expression " + filter, e);
|
||||
throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e);
|
||||
} catch (TimeoutException e) {
|
||||
throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e);
|
||||
}
|
||||
|
||||
@@ -55,12 +55,9 @@ public class FeedEntryService {
|
||||
}
|
||||
|
||||
public List<Entry> removeExistingEntries(Feed feed, List<Entry> entries) {
|
||||
Set<String> guidHashes =
|
||||
entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet());
|
||||
Set<String> guidHashes = entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet());
|
||||
Set<String> existingGuidHashes = feedEntryDAO.findExistingGuidHashes(guidHashes, feed);
|
||||
return entries.stream()
|
||||
.filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid())))
|
||||
.toList();
|
||||
return entries.stream().filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid()))).toList();
|
||||
}
|
||||
|
||||
public boolean applyFilter(FeedSubscription sub, FeedEntry entry) {
|
||||
@@ -122,37 +119,22 @@ public class FeedEntryService {
|
||||
List<FeedEntryKeyword> keywords) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subscriptions,
|
||||
true,
|
||||
keywords,
|
||||
null,
|
||||
-1,
|
||||
-1,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
user, subscriptions, true, keywords, null, -1, -1, null, false, null, null, null);
|
||||
markList(statuses, olderThan, insertedBefore);
|
||||
}
|
||||
|
||||
public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false);
|
||||
markList(statuses, olderThan, insertedBefore);
|
||||
}
|
||||
|
||||
private void markList(
|
||||
List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) {
|
||||
private void markList(List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) {
|
||||
List<FeedEntryStatus> statusesToMark =
|
||||
statuses.stream()
|
||||
.filter(FeedEntryStatus::isMarkable)
|
||||
.filter(s -> {
|
||||
Instant entryDate = s.getEntry().getPublished();
|
||||
return olderThan == null
|
||||
|| entryDate == null
|
||||
|| entryDate.isBefore(olderThan);
|
||||
return olderThan == null || entryDate == null || entryDate.isBefore(olderThan);
|
||||
})
|
||||
.filter(s -> {
|
||||
Instant insertedDate = s.getEntry().getInserted();
|
||||
|
||||
@@ -36,16 +36,14 @@ public class FeedEntryTagService {
|
||||
}
|
||||
|
||||
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
|
||||
Set<String> existingTagNames =
|
||||
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());
|
||||
Set<String> existingTagNames = existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());
|
||||
|
||||
List<FeedEntryTag> addList =
|
||||
tagNames.stream()
|
||||
.filter(name -> !existingTagNames.contains(name))
|
||||
.map(name -> new FeedEntryTag(user, entry, name))
|
||||
.toList();
|
||||
List<FeedEntryTag> removeList =
|
||||
existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList();
|
||||
List<FeedEntryTag> removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList();
|
||||
|
||||
addList.forEach(feedEntryTagDAO::persist);
|
||||
feedEntryTagDAO.delete(removeList);
|
||||
|
||||
@@ -36,8 +36,7 @@ public class FeedFaviconService {
|
||||
this.defaultFavicon =
|
||||
new Favicon(
|
||||
Resources.toByteArray(
|
||||
Objects.requireNonNull(
|
||||
getClass().getResource("/images/default_favicon.gif"))),
|
||||
Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif"))),
|
||||
"image/gif");
|
||||
}
|
||||
|
||||
|
||||
@@ -48,15 +48,13 @@ public class FeedSubscriptionService {
|
||||
// by the feed engine
|
||||
feedSubscriptionDAO.onPostCommitInsert(sub -> {
|
||||
Feed feed = sub.getFeed();
|
||||
if (feed.getDisabledUntil() == null
|
||||
|| feed.getDisabledUntil().isBefore(Instant.now())) {
|
||||
if (feed.getDisabledUntil() == null || feed.getDisabledUntil().isBefore(Instant.now())) {
|
||||
feedRefreshEngine.refreshImmediately(feed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public long subscribe(
|
||||
User user, String url, String title, FeedCategory category, int position) {
|
||||
public long subscribe(User user, String url, String title, FeedCategory category, int position) {
|
||||
Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
|
||||
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
|
||||
String message =
|
||||
@@ -98,9 +96,7 @@ public class FeedSubscriptionService {
|
||||
public void refreshAll(User user) throws ForceFeedRefreshTooSoonException {
|
||||
Instant lastForceRefresh = user.getLastForceRefresh();
|
||||
if (lastForceRefresh != null
|
||||
&& lastForceRefresh
|
||||
.plus(config.feedRefresh().forceRefreshCooldownDuration())
|
||||
.isAfter(Instant.now())) {
|
||||
&& lastForceRefresh.plus(config.feedRefresh().forceRefreshCooldownDuration()).isAfter(Instant.now())) {
|
||||
throw new ForceFeedRefreshTooSoonException();
|
||||
}
|
||||
|
||||
|
||||
@@ -47,16 +47,13 @@ public class PushNotificationService {
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public PushNotificationService(
|
||||
HttpClientFactory httpClientFactory,
|
||||
MetricRegistry metrics,
|
||||
CommaFeedConfiguration config) {
|
||||
HttpClientFactory httpClientFactory, MetricRegistry metrics, CommaFeedConfiguration config) {
|
||||
this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads());
|
||||
this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify"));
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void notify(
|
||||
PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) {
|
||||
public void notify(PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) {
|
||||
if (!config.pushNotifications().enabled() || settings.getType() == null) {
|
||||
return;
|
||||
}
|
||||
@@ -79,9 +76,7 @@ public class PushNotificationService {
|
||||
case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl);
|
||||
case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl);
|
||||
case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl);
|
||||
default ->
|
||||
throw new IllegalStateException(
|
||||
"unsupported notification type: " + settings.getType());
|
||||
default -> throw new IllegalStateException("unsupported notification type: " + settings.getType());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new PushNotificationException("Failed to send external notification", e);
|
||||
@@ -90,11 +85,7 @@ public class PushNotificationService {
|
||||
meter.mark();
|
||||
}
|
||||
|
||||
private void sendNtfy(
|
||||
PushNotificationUserSettings settings,
|
||||
String feedTitle,
|
||||
String entryTitle,
|
||||
String entryUrl)
|
||||
private void sendNtfy(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl)
|
||||
throws IOException {
|
||||
String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl());
|
||||
String topic = settings.getTopic();
|
||||
@@ -119,11 +110,7 @@ public class PushNotificationService {
|
||||
send("ntfy", request.build());
|
||||
}
|
||||
|
||||
private void sendGotify(
|
||||
PushNotificationUserSettings settings,
|
||||
String feedTitle,
|
||||
String entryTitle,
|
||||
String entryUrl)
|
||||
private void sendGotify(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl)
|
||||
throws IOException {
|
||||
String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl());
|
||||
String token = settings.getUserSecret();
|
||||
@@ -143,8 +130,7 @@ public class PushNotificationService {
|
||||
new JsonObject()
|
||||
.put(
|
||||
"client::notification",
|
||||
new JsonObject()
|
||||
.put("click", new JsonObject().put("url", entryUrl))));
|
||||
new JsonObject().put("click", new JsonObject().put("url", entryUrl))));
|
||||
}
|
||||
|
||||
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/message");
|
||||
@@ -155,10 +141,7 @@ public class PushNotificationService {
|
||||
}
|
||||
|
||||
private void sendPushover(
|
||||
PushNotificationUserSettings settings,
|
||||
String feedTitle,
|
||||
String entryTitle,
|
||||
String entryUrl)
|
||||
PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl)
|
||||
throws IOException {
|
||||
String token = settings.getUserSecret();
|
||||
String userKey = settings.getUserId();
|
||||
@@ -177,8 +160,7 @@ public class PushNotificationService {
|
||||
params.add(new BasicNameValuePair("url", entryUrl));
|
||||
}
|
||||
|
||||
SimpleRequestBuilder request =
|
||||
SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json");
|
||||
SimpleRequestBuilder request = SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json");
|
||||
request.setBody(
|
||||
WWWFormCodec.format(params, StandardCharsets.UTF_8),
|
||||
ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8));
|
||||
@@ -194,11 +176,9 @@ public class PushNotificationService {
|
||||
null);
|
||||
try {
|
||||
HttpResponse response =
|
||||
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS)
|
||||
.getHead();
|
||||
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS).getHead();
|
||||
if (response.getCode() >= 400) {
|
||||
throw new PushNotificationException(
|
||||
name + " notification failed with status " + response.getCode());
|
||||
throw new PushNotificationException(name + " notification failed with status " + response.getCode());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
future.cancel(true);
|
||||
|
||||
@@ -53,8 +53,7 @@ public class UserService {
|
||||
user = userDAO.findByEmail(nameOrEmail);
|
||||
}
|
||||
if (user != null && !user.isDisabled()) {
|
||||
boolean authenticated =
|
||||
encryptionService.authenticate(password, user.getPassword(), user.getSalt());
|
||||
boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt());
|
||||
if (authenticated) {
|
||||
performPostLoginActivities(user);
|
||||
return Optional.of(user);
|
||||
@@ -107,16 +106,11 @@ public class UserService {
|
||||
}
|
||||
|
||||
public User register(
|
||||
String name,
|
||||
String password,
|
||||
String email,
|
||||
Collection<Role> roles,
|
||||
boolean forceRegistration) {
|
||||
String name, String password, String email, Collection<Role> roles, boolean forceRegistration) {
|
||||
|
||||
if (!forceRegistration) {
|
||||
Preconditions.checkState(
|
||||
config.users().allowRegistrations(),
|
||||
"Registrations are closed on this CommaFeed instance");
|
||||
config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance");
|
||||
}
|
||||
|
||||
Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
|
||||
@@ -167,9 +161,7 @@ public class UserService {
|
||||
}
|
||||
|
||||
public String generateApiKey(User user) {
|
||||
byte[] key =
|
||||
encryptionService.getEncryptedPassword(
|
||||
UUID.randomUUID().toString(), user.getSalt());
|
||||
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
|
||||
return Digests.sha1Hex(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,18 +71,15 @@ public class DatabaseCleaningService {
|
||||
return;
|
||||
}
|
||||
|
||||
entriesDeleted =
|
||||
unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
||||
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
||||
entriesDeletedMeter.mark(entriesDeleted);
|
||||
entriesTotal += entriesDeleted;
|
||||
log.debug("removed {} entries for feeds without subscriptions", entriesTotal);
|
||||
} while (entriesDeleted > 0);
|
||||
}
|
||||
deleted =
|
||||
unitOfWork.call(() ->
|
||||
feedDAO.delete(
|
||||
feedDAO.findByIds(
|
||||
feeds.stream().map(AbstractModel::getId).toList())));
|
||||
unitOfWork.call(
|
||||
() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList())));
|
||||
total += deleted;
|
||||
log.debug("removed {} feeds without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
@@ -117,8 +114,7 @@ public class DatabaseCleaningService {
|
||||
|
||||
List<FeedCapacity> feeds =
|
||||
unitOfWork.call(() ->
|
||||
feedEntryDAO.findFeedsExceedingCapacity(
|
||||
maxFeedCapacity, batchSize, keepStarredEntries));
|
||||
feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
|
||||
if (feeds.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -128,8 +124,7 @@ public class DatabaseCleaningService {
|
||||
int deleted;
|
||||
do {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info(
|
||||
"interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||
log.info("interrupted, stopping cleanup of entries exceeding feed capacity");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,9 +132,7 @@ public class DatabaseCleaningService {
|
||||
deleted =
|
||||
unitOfWork.call(() ->
|
||||
feedEntryDAO.deleteOldEntries(
|
||||
feed.id(),
|
||||
Math.min(batchSize, rem),
|
||||
keepStarredEntries));
|
||||
feed.id(), Math.min(batchSize, rem), keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
remaining -= deleted;
|
||||
@@ -161,9 +154,8 @@ public class DatabaseCleaningService {
|
||||
}
|
||||
|
||||
deleted =
|
||||
unitOfWork.call(() ->
|
||||
feedEntryDAO.deleteEntriesOlderThan(
|
||||
olderThan, batchSize, keepStarredEntries));
|
||||
unitOfWork.call(
|
||||
() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
log.debug("removed {} old entries", total);
|
||||
@@ -181,9 +173,7 @@ public class DatabaseCleaningService {
|
||||
return;
|
||||
}
|
||||
|
||||
deleted =
|
||||
unitOfWork.call(
|
||||
() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
||||
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
||||
total += deleted;
|
||||
log.debug("removed {} old read statuses", total);
|
||||
} while (deleted != 0);
|
||||
|
||||
@@ -27,14 +27,12 @@ public class DatabaseStartupService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a postgresql database in liquibase that doesn't escape columns, so that we can use
|
||||
* lower case columns
|
||||
* Register a postgresql database in liquibase that doesn't escape columns, so that we can use lower case columns
|
||||
*/
|
||||
@MetaInfServices(Database.class)
|
||||
public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase {
|
||||
@Override
|
||||
public String escapeObjectName(
|
||||
String objectName, Class<? extends DatabaseObject> objectType) {
|
||||
public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ import java.util.List;
|
||||
public class YoutubeFeedURLProvider implements FeedURLProvider {
|
||||
|
||||
private static final String PREFIX = "https://www.youtube.com/channel/";
|
||||
private static final String REPLACEMENT_PREFIX =
|
||||
"https://www.youtube.com/feeds/videos.xml?channel_id=";
|
||||
private static final String REPLACEMENT_PREFIX = "https://www.youtube.com/feeds/videos.xml?channel_id=";
|
||||
|
||||
@Override
|
||||
public List<String> get(String url, String urlContent) {
|
||||
|
||||
@@ -67,10 +67,7 @@ public class Entry implements Serializable {
|
||||
@Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true)
|
||||
private Instant date;
|
||||
|
||||
@Schema(
|
||||
description = "entry insertion date in the database",
|
||||
type = SchemaType.INTEGER,
|
||||
required = true)
|
||||
@Schema(description = "entry insertion date in the database", type = SchemaType.INTEGER, required = true)
|
||||
private Instant insertedDate;
|
||||
|
||||
@Schema(description = "feed id", required = true)
|
||||
@@ -97,9 +94,7 @@ public class Entry implements Serializable {
|
||||
@Schema(description = "starred status", required = true)
|
||||
private boolean starred;
|
||||
|
||||
@Schema(
|
||||
description = "whether the entry is still markable (old entry statuses are discarded)",
|
||||
required = true)
|
||||
@Schema(description = "whether the entry is still markable (old entry statuses are discarded)", required = true)
|
||||
private boolean markable;
|
||||
|
||||
@Schema(description = "tags", required = true)
|
||||
@@ -130,10 +125,7 @@ public class Entry implements Serializable {
|
||||
if (content != null) {
|
||||
entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL);
|
||||
entry.setTitle(content.getTitle());
|
||||
entry.setContent(
|
||||
proxyImages
|
||||
? FeedUtils.proxyImages(content.getContent())
|
||||
: content.getContent());
|
||||
entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent()) : content.getContent());
|
||||
entry.setAuthor(content.getAuthor());
|
||||
|
||||
entry.setEnclosureType(content.getEnclosureType());
|
||||
|
||||
@@ -29,14 +29,10 @@ public class Settings implements Serializable {
|
||||
@Schema(description = "user reads entries in ascending or descending order", required = true)
|
||||
private ReadingOrder readingOrder;
|
||||
|
||||
@Schema(
|
||||
description = "user wants category and feeds with no unread entries shown",
|
||||
required = true)
|
||||
@Schema(description = "user wants category and feeds with no unread entries shown", required = true)
|
||||
private boolean showRead;
|
||||
|
||||
@Schema(
|
||||
description = "In expanded view, scroll through entries mark them as read",
|
||||
required = true)
|
||||
@Schema(description = "In expanded view, scroll through entries mark them as read", required = true)
|
||||
private boolean scrollMarks;
|
||||
|
||||
@Schema(description = "user's custom css for the website")
|
||||
@@ -45,42 +41,33 @@ public class Settings implements Serializable {
|
||||
@Schema(description = "user's custom js for the website")
|
||||
private String customJs;
|
||||
|
||||
@Schema(
|
||||
description = "user's preferred scroll speed when navigating between entries",
|
||||
required = true)
|
||||
@Schema(description = "user's preferred scroll speed when navigating between entries", required = true)
|
||||
private int scrollSpeed;
|
||||
|
||||
@Schema(description = "whether to scroll to the selected entry", required = true)
|
||||
private ScrollMode scrollMode;
|
||||
|
||||
@Schema(
|
||||
description = "number of entries to keep above the selected entry when scrolling",
|
||||
required = true)
|
||||
@Schema(description = "number of entries to keep above the selected entry when scrolling", required = true)
|
||||
private int entriesToKeepOnTopWhenScrolling;
|
||||
|
||||
@Schema(description = "whether to show the star icon in the header of entries", required = true)
|
||||
private IconDisplayMode starIconDisplayMode;
|
||||
|
||||
@Schema(
|
||||
description = "whether to show the external link icon in the header of entries",
|
||||
required = true)
|
||||
@Schema(description = "whether to show the external link icon in the header of entries", required = true)
|
||||
private IconDisplayMode externalLinkIconDisplayMode;
|
||||
|
||||
@Schema(description = "ask for confirmation when marking all entries as read", required = true)
|
||||
private boolean markAllAsReadConfirmation;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"navigate to the next unread category or feed after marking all entries as read",
|
||||
description = "navigate to the next unread category or feed after marking all entries as read",
|
||||
required = true)
|
||||
private boolean markAllAsReadNavigateToNextUnread;
|
||||
|
||||
@Schema(description = "show commafeed's own context menu on right click", required = true)
|
||||
private boolean customContextMenu;
|
||||
|
||||
@Schema(
|
||||
description = "on mobile, show action buttons at the bottom of the screen",
|
||||
required = true)
|
||||
@Schema(description = "on mobile, show action buttons at the bottom of the screen", required = true)
|
||||
private boolean mobileFooter;
|
||||
|
||||
@Schema(description = "show unread count in the title", required = true)
|
||||
|
||||
@@ -37,8 +37,7 @@ public class Subscription implements Serializable {
|
||||
private Instant lastRefresh;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"next time the feed refresh is planned, null if refresh is already queued",
|
||||
description = "next time the feed refresh is planned, null if refresh is already queued",
|
||||
type = SchemaType.INTEGER)
|
||||
private Instant nextRefresh;
|
||||
|
||||
@@ -63,22 +62,16 @@ public class Subscription implements Serializable {
|
||||
@Schema(description = "date of the newest item", type = SchemaType.INTEGER)
|
||||
private Instant newestItemTime;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
private String filter;
|
||||
|
||||
@Schema(description = "JEXL legacy filter")
|
||||
private String filterLegacy;
|
||||
|
||||
@Schema(
|
||||
description = "whether to send push notifications for new entries of this feed",
|
||||
required = true)
|
||||
@Schema(description = "whether to send push notifications for new entries of this feed", required = true)
|
||||
private boolean pushNotificationsEnabled;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"automatically mark entries as read after this many days (null to disable)")
|
||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
|
||||
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
|
||||
|
||||
@@ -27,17 +27,13 @@ public class FeedModificationRequest implements Serializable {
|
||||
@Schema(description = "new display position, null if not changed")
|
||||
private Integer position;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match")
|
||||
@Size(max = 4096)
|
||||
private String filter;
|
||||
|
||||
@Schema(description = "whether to send push notifications for new entries of this feed")
|
||||
private boolean pushNotificationsEnabled;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"automatically mark entries as read after this many days (null to disable)")
|
||||
@Schema(description = "automatically mark entries as read after this many days (null to disable)")
|
||||
private Integer autoMarkAsReadAfterDays;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ public class MarkRequest implements Serializable {
|
||||
@Size(max = 128)
|
||||
private String keywords;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"if marking a category or 'all', exclude those subscriptions from the marking")
|
||||
@Schema(description = "if marking a category or 'all', exclude those subscriptions from the marking")
|
||||
private List<Long> excludedSubscriptions;
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ public class AdminREST {
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Save or update a user",
|
||||
description =
|
||||
"Save or update a user. If the id is not specified, a new user will be created")
|
||||
description = "Save or update a user. If the id is not specified, a new user will be created")
|
||||
public Response adminSaveUser(@Valid @Parameter(required = true) AdminSaveUserRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
@@ -77,24 +76,14 @@ public class AdminREST {
|
||||
roles.add(Role.ADMIN);
|
||||
}
|
||||
try {
|
||||
id =
|
||||
userService
|
||||
.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
roles,
|
||||
true)
|
||||
.getId();
|
||||
id = userService.register(req.getName(), req.getPassword(), req.getEmail(), roles, true).getId();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
||||
}
|
||||
} else {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("You cannot disable your own account.")
|
||||
.build();
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
|
||||
}
|
||||
|
||||
User u = userDAO.findById(id);
|
||||
@@ -134,8 +123,7 @@ public class AdminREST {
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(summary = "Get user information", description = "Get user information")
|
||||
public UserModel adminGetUser(
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
public UserModel adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User u = userDAO.findById(id);
|
||||
UserModel userModel = new UserModel();
|
||||
@@ -143,8 +131,7 @@ public class AdminREST {
|
||||
userModel.setName(u.getName());
|
||||
userModel.setEmail(u.getEmail());
|
||||
userModel.setEnabled(!u.isDisabled());
|
||||
userModel.setAdmin(
|
||||
userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN));
|
||||
userModel.setAdmin(userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN));
|
||||
return userModel;
|
||||
}
|
||||
|
||||
@@ -192,9 +179,7 @@ public class AdminREST {
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user.getId().equals(u.getId())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("You cannot delete your own user.")
|
||||
.build();
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build();
|
||||
}
|
||||
userService.unregister(u);
|
||||
return Response.ok().build();
|
||||
|
||||
@@ -104,30 +104,22 @@ public class CategoryREST {
|
||||
@Operation(summary = "Get category entries", description = "Get a list of category entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Entries.class))
|
||||
})
|
||||
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class))})
|
||||
@APIResponse(responseCode = "404", description = "category not found")
|
||||
public Response getCategoryEntries(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true)
|
||||
@QueryParam("id")
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id")
|
||||
String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("unread")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@@ -136,13 +128,11 @@ public class CategoryREST {
|
||||
@Parameter(description = "comma-separated list of excluded subscription ids")
|
||||
@QueryParam("excludedSubscriptionIds")
|
||||
String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag")
|
||||
String tag) {
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
|
||||
Preconditions.checkNotNull(readType);
|
||||
|
||||
List<FeedEntryKeyword> entryKeywords =
|
||||
FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
@@ -159,8 +149,7 @@ public class CategoryREST {
|
||||
|
||||
List<Long> excludedIds = null;
|
||||
if (StringUtils.isNotEmpty(excludedSubscriptionIds)) {
|
||||
excludedIds =
|
||||
Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
|
||||
excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
@@ -191,18 +180,15 @@ public class CategoryREST {
|
||||
} else if (STARRED.equals(id)) {
|
||||
entries.setName("Starred");
|
||||
List<FeedEntryStatus> starred =
|
||||
feedEntryStatusDAO.findStarred(
|
||||
user, entryKeywords, newerThanDate, offset, limit + 1, order, true);
|
||||
feedEntryStatusDAO.findStarred(user, entryKeywords, newerThanDate, offset, limit + 1, order, true);
|
||||
for (FeedEntryStatus status : starred) {
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id));
|
||||
if (parent != null) {
|
||||
List<FeedCategory> categories =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs =
|
||||
feedSubscriptionDAO.findByCategories(user, categories);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
@@ -224,9 +210,7 @@ public class CategoryREST {
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND)
|
||||
.entity("<message>category not found</message>")
|
||||
.build();
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,28 +228,22 @@ public class CategoryREST {
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get category entries as feed",
|
||||
description = "Get a feed of category entries")
|
||||
@Operation(summary = "Get category entries as feed", description = "Get a feed of category entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getCategoryEntriesAsFeed(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true)
|
||||
@QueryParam("id")
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id")
|
||||
String id,
|
||||
@Parameter(description = "all entries or only unread ones", required = true)
|
||||
@DefaultValue("all")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@@ -274,20 +252,11 @@ public class CategoryREST {
|
||||
@Parameter(description = "comma-separated list of excluded subscription ids")
|
||||
@QueryParam("excludedSubscriptionIds")
|
||||
String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag")
|
||||
String tag) {
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
|
||||
Response response =
|
||||
getCategoryEntries(
|
||||
id,
|
||||
readType,
|
||||
newerThan,
|
||||
offset,
|
||||
limit,
|
||||
order,
|
||||
keywords,
|
||||
excludedSubscriptionIds,
|
||||
tag);
|
||||
id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
@@ -314,21 +283,14 @@ public class CategoryREST {
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark category entries",
|
||||
description = "Mark feed entries of this category as read")
|
||||
@Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read")
|
||||
public Response markCategoryEntries(
|
||||
@Valid @Parameter(description = "category id, or 'all'", required = true)
|
||||
MarkRequest req) {
|
||||
@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
Instant olderThan =
|
||||
req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore =
|
||||
req.getInsertedBefore() == null
|
||||
? null
|
||||
: Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
@@ -336,8 +298,7 @@ public class CategoryREST {
|
||||
if (ALL.equals(req.getId())) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
} else if (STARRED.equals(req.getId())) {
|
||||
feedEntryService.markStarredEntries(user, olderThan, insertedBefore);
|
||||
} else {
|
||||
@@ -345,8 +306,7 @@ public class CategoryREST {
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
@@ -400,8 +360,7 @@ public class CategoryREST {
|
||||
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, cat);
|
||||
for (FeedCategory child : categories) {
|
||||
if (!child.getId().equals(cat.getId())
|
||||
&& child.getParent().getId().equals(cat.getId())) {
|
||||
if (!child.getId().equals(cat.getId()) && child.getParent().getId().equals(cat.getId())) {
|
||||
child.setParent(null);
|
||||
}
|
||||
}
|
||||
@@ -417,8 +376,7 @@ public class CategoryREST {
|
||||
@Path("/modify")
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a category", description = "Modify an existing feed category")
|
||||
public Response modifyCategory(
|
||||
@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -437,14 +395,11 @@ public class CategoryREST {
|
||||
}
|
||||
if (parent != null) {
|
||||
Long parentId = parent.getId();
|
||||
List<FeedCategory> descendants =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
boolean parentIsDescendant =
|
||||
descendants.stream().anyMatch(c -> c.getId().equals(parentId));
|
||||
List<FeedCategory> descendants = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
boolean parentIsDescendant = descendants.stream().anyMatch(c -> c.getId().equals(parentId));
|
||||
if (parentIsDescendant) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity(
|
||||
"<message>cannot set a category's own descendant as its parent</message>")
|
||||
.entity("<message>cannot set a category's own descendant as its parent</message>")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -476,9 +431,7 @@ public class CategoryREST {
|
||||
@POST
|
||||
@Path("/collapse")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Collapse a category",
|
||||
description = "Save collapsed or expanded status for a category")
|
||||
@Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category")
|
||||
public Response collapseCategory(@Parameter(required = true) CollapseRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
@@ -506,9 +459,7 @@ public class CategoryREST {
|
||||
@GET
|
||||
@Path("/get")
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get root category",
|
||||
description = "Get all categories and subscriptions of the user")
|
||||
@Operation(summary = "Get root category", description = "Get all categories and subscriptions of the user")
|
||||
public Category getRootCategory() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
@@ -551,8 +502,7 @@ public class CategoryREST {
|
||||
|
||||
for (FeedSubscription subscription : subscriptions) {
|
||||
if (id == null && subscription.getCategory() == null
|
||||
|| subscription.getCategory() != null
|
||||
&& Objects.equals(subscription.getCategory().getId(), id)) {
|
||||
|| subscription.getCategory() != null && Objects.equals(subscription.getCategory().getId(), id)) {
|
||||
UnreadCount uc = unreadCount.get(subscription.getId());
|
||||
Subscription sub = Subscription.build(subscription, uc);
|
||||
category.getFeeds().add(sub);
|
||||
|
||||
@@ -50,8 +50,7 @@ public class EntryREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response markEntry(
|
||||
@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -63,12 +62,9 @@ public class EntryREST {
|
||||
@Path("/markMultiple")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark multiple feed entries",
|
||||
description = "Mark feed entries as read/unread")
|
||||
@Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread")
|
||||
public Response markEntries(
|
||||
@Valid @Parameter(description = "Multiple Mark Request", required = true)
|
||||
MultipleMarkRequest req) {
|
||||
@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getRequests());
|
||||
|
||||
@@ -85,8 +81,7 @@ public class EntryREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread")
|
||||
public Response starEntry(
|
||||
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -99,9 +94,7 @@ public class EntryREST {
|
||||
@Path("/tags")
|
||||
@GET
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get list of tags for the user",
|
||||
description = "Get list of tags for the user")
|
||||
@Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user")
|
||||
public Response getTags() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
List<String> tags = feedEntryTagDAO.findByUser(user);
|
||||
@@ -112,8 +105,7 @@ public class EntryREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Set feed entry tags")
|
||||
public Response tagEntry(
|
||||
@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getEntryId());
|
||||
|
||||
|
||||
@@ -133,11 +133,7 @@ public class FeedREST {
|
||||
@Operation(summary = "Get feed entries", description = "Get a list of feed entries")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Entries.class))
|
||||
})
|
||||
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class))})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeedEntries(
|
||||
@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@@ -145,16 +141,13 @@ public class FeedREST {
|
||||
@DefaultValue("unread")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@@ -164,8 +157,7 @@ public class FeedREST {
|
||||
Preconditions.checkNotNull(id);
|
||||
Preconditions.checkNotNull(readType);
|
||||
|
||||
List<FeedEntryKeyword> entryKeywords =
|
||||
FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords));
|
||||
|
||||
limit = Math.min(limit, 1000);
|
||||
limit = Math.max(0, limit);
|
||||
@@ -211,9 +203,7 @@ public class FeedREST {
|
||||
entries.getEntries().removeLast();
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND)
|
||||
.entity("<message>feed not found</message>")
|
||||
.build();
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>feed not found</message>").build();
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
@@ -232,16 +222,13 @@ public class FeedREST {
|
||||
@DefaultValue("all")
|
||||
@QueryParam("readType")
|
||||
ReadingMode readType,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan")
|
||||
Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset")
|
||||
int offset,
|
||||
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
|
||||
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
|
||||
@Parameter(description = "limit for paging, default 20, maximum 1000")
|
||||
@DefaultValue("20")
|
||||
@QueryParam("limit")
|
||||
int limit,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc")
|
||||
ReadingOrder order,
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
|
||||
@Parameter(
|
||||
description =
|
||||
"search for keywords in either the title or the content of the entries, separated by spaces")
|
||||
@@ -277,8 +264,7 @@ public class FeedREST {
|
||||
url = StringUtils.trimToEmpty(url);
|
||||
url = prependHttp(url);
|
||||
try {
|
||||
FeedFetcherResult feedFetcherResult =
|
||||
feedFetcher.fetch(url, true, null, null, null, null);
|
||||
FeedFetcherResult feedFetcherResult = feedFetcher.fetch(url, true, null, null, null, null);
|
||||
info = new FeedInfo();
|
||||
info.setUrl(feedFetcherResult.urlAfterRedirect());
|
||||
info.setTitle(feedFetcherResult.feed().title());
|
||||
@@ -296,14 +282,9 @@ public class FeedREST {
|
||||
@Operation(summary = "Fetch a feed", description = "Fetch a feed by its url")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = FeedInfo.class))
|
||||
})
|
||||
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = FeedInfo.class))})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response fetchFeed(
|
||||
@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
|
||||
@@ -339,33 +320,21 @@ public class FeedREST {
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Mark feed entries",
|
||||
description = "Mark feed entries as read (unread is not supported)")
|
||||
public Response markFeedEntries(
|
||||
@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
@Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)")
|
||||
public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
Instant olderThan =
|
||||
req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore =
|
||||
req.getInsertedBefore() == null
|
||||
? null
|
||||
: Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan());
|
||||
Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore());
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription =
|
||||
feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
|
||||
if (subscription != null) {
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user,
|
||||
Collections.singletonList(subscription),
|
||||
olderThan,
|
||||
insertedBefore,
|
||||
entryKeywords);
|
||||
user, Collections.singletonList(subscription), olderThan, insertedBefore, entryKeywords);
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
@@ -376,14 +345,9 @@ public class FeedREST {
|
||||
@Operation(summary = "get feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Subscription.class))
|
||||
})
|
||||
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))})
|
||||
@APIResponse(responseCode = "404", description = "feed not found")
|
||||
public Response getFeed(
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
@@ -435,14 +399,9 @@ public class FeedREST {
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))
|
||||
})
|
||||
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Long.class))})
|
||||
public Response subscribe(
|
||||
@Valid @Parameter(description = "subscription request", required = true)
|
||||
SubscribeRequest req) {
|
||||
@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getTitle());
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
@@ -455,9 +414,7 @@ public class FeedREST {
|
||||
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
long subscriptionId =
|
||||
feedSubscriptionService.subscribe(
|
||||
user, info.getUrl(), req.getTitle(), category, 0);
|
||||
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0);
|
||||
return Response.ok(subscriptionId).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e);
|
||||
@@ -513,18 +470,14 @@ public class FeedREST {
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a subscription", description = "Modify a feed subscription")
|
||||
public Response modifyFeed(
|
||||
@Valid @Parameter(description = "subscription id", required = true)
|
||||
FeedModificationRequest req) {
|
||||
@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
try {
|
||||
feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY);
|
||||
} catch (FeedEntryFilterException e) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
@@ -576,22 +529,15 @@ public class FeedREST {
|
||||
@Path("/import")
|
||||
@Transactional
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(
|
||||
summary = "OPML import",
|
||||
description = "Import an OPML file, posted as a FORM with the 'file' name")
|
||||
public Response importOpml(
|
||||
@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
|
||||
@Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name")
|
||||
public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("Import is disabled for the demo account")
|
||||
.build();
|
||||
return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
|
||||
}
|
||||
try {
|
||||
// opml will be encoded in the default JVM encoding, bu we want UTF-8
|
||||
opmlImporter.importOpml(
|
||||
user,
|
||||
new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8));
|
||||
opmlImporter.importOpml(user, new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
}
|
||||
@@ -602,9 +548,7 @@ public class FeedREST {
|
||||
@Path("/export")
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Operation(
|
||||
summary = "OPML export",
|
||||
description = "Export an OPML file of the user's subscriptions")
|
||||
@Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions")
|
||||
public Response exportOpml() throws FeedException {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Opml opml = opmlExporter.export(user);
|
||||
|
||||
@@ -58,8 +58,7 @@ public class ServerREST {
|
||||
infos.setWebsocketEnabled(config.websocket().enabled());
|
||||
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
||||
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
|
||||
infos.setForceRefreshCooldownDuration(
|
||||
config.feedRefresh().forceRefreshCooldownDuration().toMillis());
|
||||
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
|
||||
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
|
||||
infos.setMinimumPasswordLength(config.users().minimumPasswordLength());
|
||||
infos.setPushNotificationsEnabled(config.pushNotifications().enabled());
|
||||
|
||||
@@ -134,14 +134,10 @@ public class UserREST {
|
||||
|
||||
if (settings.getPushNotifications() != null) {
|
||||
s.getPushNotificationSettings().setType(settings.getPushNotifications().getType());
|
||||
s.getPushNotificationSettings()
|
||||
.setServerUrl(settings.getPushNotifications().getServerUrl());
|
||||
s.getPushNotificationSettings()
|
||||
.setUserId(settings.getPushNotifications().getUserId());
|
||||
s.getPushNotificationSettings()
|
||||
.setUserSecret(settings.getPushNotifications().getUserSecret());
|
||||
s.getPushNotificationSettings()
|
||||
.setTopic(settings.getPushNotifications().getTopic());
|
||||
s.getPushNotificationSettings().setServerUrl(settings.getPushNotifications().getServerUrl());
|
||||
s.getPushNotificationSettings().setUserId(settings.getPushNotifications().getUserId());
|
||||
s.getPushNotificationSettings().setUserSecret(settings.getPushNotifications().getUserSecret());
|
||||
s.getPushNotificationSettings().setTopic(settings.getPushNotifications().getTopic());
|
||||
}
|
||||
} else {
|
||||
s.setReadingMode(ReadingMode.UNREAD);
|
||||
@@ -191,10 +187,7 @@ public class UserREST {
|
||||
s.setShowRead(settings.isShowRead());
|
||||
s.setScrollMarks(settings.isScrollMarks());
|
||||
s.setCustomCss(settings.getCustomCss());
|
||||
s.setCustomJs(
|
||||
CommaFeedConstants.USERNAME_DEMO.equals(user.getName())
|
||||
? ""
|
||||
: settings.getCustomJs());
|
||||
s.setCustomJs(CommaFeedConstants.USERNAME_DEMO.equals(user.getName()) ? "" : settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setScrollMode(settings.getScrollMode());
|
||||
@@ -234,8 +227,7 @@ public class UserREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Send a test push notification")
|
||||
public Response sendTestPushNotification(
|
||||
@Parameter(required = true) PushNotificationSettings settings) {
|
||||
public Response sendTestPushNotification(@Parameter(required = true) PushNotificationSettings settings) {
|
||||
FeedSubscription sub = new FeedSubscription();
|
||||
sub.setTitle("CommaFeed Test Feed");
|
||||
sub.setFeed(new Feed());
|
||||
@@ -291,8 +283,7 @@ public class UserREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Save user's profile")
|
||||
public Response saveUserProfile(
|
||||
@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
@@ -335,10 +326,7 @@ public class UserREST {
|
||||
public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) {
|
||||
try {
|
||||
userService.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
Collections.singletonList(Role.USER));
|
||||
req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER));
|
||||
return Response.ok().build();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
@@ -355,17 +343,10 @@ public class UserREST {
|
||||
public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) {
|
||||
boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired();
|
||||
if (!initialSetupRequired) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity("Initial setup has already been completed")
|
||||
.build();
|
||||
return Response.status(Status.BAD_REQUEST).entity("Initial setup has already been completed").build();
|
||||
}
|
||||
|
||||
userService.register(
|
||||
req.getName(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
List.of(Role.ADMIN, Role.USER),
|
||||
true);
|
||||
userService.register(req.getName(), req.getPassword(), req.getEmail(), List.of(Role.ADMIN, Role.USER), true);
|
||||
|
||||
if (config.users().createDemoAccount()) {
|
||||
User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
|
||||
@@ -384,8 +365,7 @@ public class UserREST {
|
||||
@Operation(summary = "send a password reset email")
|
||||
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
||||
if (!config.passwordRecoveryEnabled()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Password recovery is not enabled on this CommaFeed instance");
|
||||
throw new IllegalArgumentException("Password recovery is not enabled on this CommaFeed instance");
|
||||
}
|
||||
|
||||
User user = userDAO.findByEmail(req.getEmail());
|
||||
@@ -409,8 +389,7 @@ public class UserREST {
|
||||
}
|
||||
|
||||
private String buildEmailContent(User user) throws URISyntaxException {
|
||||
String publicUrl =
|
||||
Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow());
|
||||
String publicUrl = Urls.removeTrailingSlash(config.passwordRecoveryPublicBaseUrl().orElseThrow());
|
||||
return String.format(
|
||||
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||
HtmlEscapers.htmlEscaper().escape(user.getName()), callbackUrl(user, publicUrl));
|
||||
@@ -429,8 +408,7 @@ public class UserREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "confirm password reset with new password")
|
||||
public Response passwordRecoveryCallback(
|
||||
@Valid @Parameter(required = true) PasswordResetConfirmationRequest req) {
|
||||
public Response passwordRecoveryCallback(@Valid @Parameter(required = true) PasswordResetConfirmationRequest req) {
|
||||
String email = req.getEmail();
|
||||
String token = req.getToken();
|
||||
String password = req.getPassword();
|
||||
@@ -440,12 +418,8 @@ public class UserREST {
|
||||
Preconditions.checkNotNull(password);
|
||||
|
||||
User user = userDAO.findByEmail(email);
|
||||
if (user == null
|
||||
|| user.getRecoverPasswordToken() == null
|
||||
|| !user.getRecoverPasswordToken().equals(token)) {
|
||||
return Response.status(Status.UNAUTHORIZED)
|
||||
.entity("Email not found or invalid token.")
|
||||
.build();
|
||||
if (user == null || user.getRecoverPasswordToken() == null || !user.getRecoverPasswordToken().equals(token)) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Email not found or invalid token.").build();
|
||||
}
|
||||
if (ChronoUnit.MINUTES.between(user.getRecoverPasswordTokenDate(), Instant.now()) >= 30) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build();
|
||||
@@ -468,16 +442,12 @@ public class UserREST {
|
||||
public Response deleteUser() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("the demo account cannot be deleted")
|
||||
.build();
|
||||
return Response.status(Status.FORBIDDEN).entity("the demo account cannot be deleted").build();
|
||||
}
|
||||
|
||||
Set<Role> roles = userRoleDAO.findRoles(user);
|
||||
if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) {
|
||||
return Response.status(Status.FORBIDDEN)
|
||||
.entity("The last admin account cannot be deleted")
|
||||
.build();
|
||||
return Response.status(Status.FORBIDDEN).entity("The last admin account cannot be deleted").build();
|
||||
}
|
||||
|
||||
userService.unregister(userDAO.findById(user.getId()));
|
||||
|
||||
@@ -94,9 +94,7 @@ public class FeverREST {
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formUrlencoded(
|
||||
@Context UriInfo uri,
|
||||
@PathParam("userId") Long userId,
|
||||
MultivaluedMap<String, String> form) {
|
||||
@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap<String, String> form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
@@ -135,14 +133,10 @@ public class FeverREST {
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public FeverResponse formData(
|
||||
@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
|
||||
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||
form.getValues()
|
||||
.forEach((k, v) ->
|
||||
params.put(
|
||||
k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
|
||||
form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
@@ -212,8 +206,7 @@ public class FeverREST {
|
||||
if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) {
|
||||
long id = Long.parseLong(params.get("id"));
|
||||
String before = params.get("before");
|
||||
Instant insertedBefore =
|
||||
before == null ? null : Instant.ofEpochSecond(Long.parseLong(before));
|
||||
Instant insertedBefore = before == null ? null : Instant.ofEpochSecond(Long.parseLong(before));
|
||||
mark(user, params.get("mark"), id, params.get("as"), insertedBefore);
|
||||
}
|
||||
|
||||
@@ -236,9 +229,7 @@ public class FeverREST {
|
||||
|
||||
private List<FeverFeedGroup> buildFeedsGroups(List<FeedSubscription> subscriptions) {
|
||||
return subscriptions.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
||||
.collect(Collectors.groupingBy(s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> {
|
||||
@@ -272,9 +263,7 @@ public class FeverREST {
|
||||
f.setSiteUrl(s.getFeed().getLink());
|
||||
f.setSpark(false);
|
||||
f.setLastUpdatedOnTime(
|
||||
s.getFeed().getLastUpdated() == null
|
||||
? 0
|
||||
: s.getFeed().getLastUpdated().getEpochSecond());
|
||||
s.getFeed().getLastUpdated() == null ? 0 : s.getFeed().getLastUpdated().getEpochSecond());
|
||||
return f;
|
||||
})
|
||||
.toList();
|
||||
@@ -305,8 +294,7 @@ public class FeverREST {
|
||||
return statuses.stream().map(s -> s.getEntry().getId()).toList();
|
||||
}
|
||||
|
||||
private List<FeverItem> buildItems(
|
||||
User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
|
||||
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, List<String> entryIds) {
|
||||
List<FeverItem> items = new ArrayList<>();
|
||||
|
||||
Map<Long, FeedSubscription> subscriptionsByFeedId =
|
||||
@@ -323,8 +311,7 @@ public class FeverREST {
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<FeverItem> buildItems(
|
||||
User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
|
||||
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
@@ -366,8 +353,7 @@ public class FeverREST {
|
||||
f.setData(
|
||||
String.format(
|
||||
"data:%s;base64,%s",
|
||||
favicon.mediaType(),
|
||||
Base64.getEncoder().encodeToString(favicon.icon())));
|
||||
favicon.mediaType(), Base64.getEncoder().encodeToString(favicon.icon())));
|
||||
return f;
|
||||
})
|
||||
.toList();
|
||||
@@ -387,10 +373,8 @@ public class FeverREST {
|
||||
} else if ("group".equals(source)) {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, id);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, parent);
|
||||
List<FeedSubscription> subscriptions =
|
||||
feedSubscriptionDAO.findByCategories(user, categories);
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, subscriptions, null, insertedBefore, null);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
feedEntryService.markSubscriptionEntries(user, subscriptions, null, insertedBefore, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,24 +157,18 @@ public class FeverResponse {
|
||||
@Data
|
||||
public static class FeverLink {}
|
||||
|
||||
public static class LongListToCommaSeparatedStringSerializer
|
||||
extends JsonSerializer<List<Long>> {
|
||||
public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer<List<Long>> {
|
||||
@Override
|
||||
public void serialize(
|
||||
List<Long> input,
|
||||
JsonGenerator jsonGenerator,
|
||||
SerializerProvider serializerProvider)
|
||||
public void serialize(List<Long> input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
String output = input.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
jsonGenerator.writeObject(output);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommaSeparatedStringToLongListDeserializer
|
||||
extends JsonDeserializer<List<Long>> {
|
||||
public static class CommaSeparatedStringToLongListDeserializer extends JsonDeserializer<List<Long>> {
|
||||
@Override
|
||||
public List<Long> deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException {
|
||||
public List<Long> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
String value = ctxt.readValue(p, String.class);
|
||||
return Stream.of(value.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
|
||||
@@ -145,8 +145,7 @@ public class GoogleReaderREST {
|
||||
public GoogleReaderModel.SubscriptionList subscriptionList() {
|
||||
User user = currentUser();
|
||||
GoogleReaderModel.SubscriptionList resp = new GoogleReaderModel.SubscriptionList();
|
||||
resp.setSubscriptions(
|
||||
feedSubscriptionDAO.findAll(user).stream().map(this::mapSubscription).toList());
|
||||
resp.setSubscriptions(feedSubscriptionDAO.findAll(user).stream().map(this::mapSubscription).toList());
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -216,10 +215,7 @@ public class GoogleReaderREST {
|
||||
List<GoogleReaderModel.Tag> tags = new ArrayList<>();
|
||||
feedCategoryDAO
|
||||
.findAll(user)
|
||||
.forEach(c ->
|
||||
tags.add(
|
||||
new GoogleReaderModel.Tag(
|
||||
GoogleReaderStreamId.label(c.getName()))));
|
||||
.forEach(c -> tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.label(c.getName()))));
|
||||
tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED));
|
||||
|
||||
GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList();
|
||||
@@ -256,8 +252,7 @@ public class GoogleReaderREST {
|
||||
}
|
||||
|
||||
Map<Long, FeedCategory> categoriesById =
|
||||
feedCategoryDAO.findAll(user).stream()
|
||||
.collect(Collectors.toMap(FeedCategory::getId, c -> c));
|
||||
feedCategoryDAO.findAll(user).stream().collect(Collectors.toMap(FeedCategory::getId, c -> c));
|
||||
unreadByCategory.forEach((categoryId, count) -> {
|
||||
if (categoryId == 0) {
|
||||
return;
|
||||
@@ -325,14 +320,12 @@ public class GoogleReaderREST {
|
||||
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
|
||||
boolean unreadOnly = isUnreadOnly(excludeTargets);
|
||||
|
||||
PagedStatuses paged =
|
||||
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||
PagedStatuses paged = fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||
|
||||
GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs();
|
||||
resp.setItemRefs(
|
||||
paged.statuses().stream()
|
||||
.map(s ->
|
||||
new GoogleReaderModel.ItemRef(String.valueOf(s.getEntry().getId())))
|
||||
.map(s -> new GoogleReaderModel.ItemRef(String.valueOf(s.getEntry().getId())))
|
||||
.toList());
|
||||
resp.setContinuation(paged.continuation());
|
||||
return resp;
|
||||
@@ -343,8 +336,7 @@ public class GoogleReaderREST {
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public GoogleReaderModel.StreamContents streamItemsContentsPost(
|
||||
@FormParam("i") List<String> ids) {
|
||||
public GoogleReaderModel.StreamContents streamItemsContentsPost(@FormParam("i") List<String> ids) {
|
||||
return buildStreamItemsContents(ids);
|
||||
}
|
||||
|
||||
@@ -352,8 +344,7 @@ public class GoogleReaderREST {
|
||||
@Path("/reader/api/0/stream/items/contents")
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public GoogleReaderModel.StreamContents streamItemsContentsGet(
|
||||
@QueryParam("i") List<String> ids) {
|
||||
public GoogleReaderModel.StreamContents streamItemsContentsGet(@QueryParam("i") List<String> ids) {
|
||||
return buildStreamItemsContents(ids);
|
||||
}
|
||||
|
||||
@@ -415,9 +406,7 @@ public class GoogleReaderREST {
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public Response markAllAsRead(
|
||||
@FormParam("s") String streamId,
|
||||
@FormParam("ts") Long ts,
|
||||
@FormParam("T") String token) {
|
||||
@FormParam("s") String streamId, @FormParam("ts") Long ts, @FormParam("T") String token) {
|
||||
User user = currentUser();
|
||||
if (!validToken(user, token)) {
|
||||
return Response.status(Status.UNAUTHORIZED).build();
|
||||
@@ -430,20 +419,16 @@ public class GoogleReaderREST {
|
||||
case STARRED -> feedEntryService.markStarredEntries(user, olderThan, null);
|
||||
case FEED -> {
|
||||
Long subId = parsed.feedSubscriptionId();
|
||||
FeedSubscription sub =
|
||||
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
FeedSubscription sub = subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
if (sub != null) {
|
||||
feedEntryService.markSubscriptionEntries(
|
||||
user, List.of(sub), olderThan, null, null);
|
||||
feedEntryService.markSubscriptionEntries(user, List.of(sub), olderThan, null, null);
|
||||
}
|
||||
}
|
||||
case LABEL -> {
|
||||
FeedCategory category = findCategoryByName(user, parsed.value());
|
||||
if (category != null) {
|
||||
List<FeedCategory> categories =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subs =
|
||||
feedSubscriptionDAO.findByCategories(user, categories);
|
||||
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
|
||||
feedEntryService.markSubscriptionEntries(user, subs, olderThan, null, null);
|
||||
}
|
||||
}
|
||||
@@ -456,19 +441,14 @@ public class GoogleReaderREST {
|
||||
}
|
||||
|
||||
private GoogleReaderModel.StreamContents buildStreamContents(
|
||||
String rawStreamId,
|
||||
Integer n,
|
||||
String continuation,
|
||||
String order,
|
||||
List<String> excludeTargets) {
|
||||
String rawStreamId, Integer n, String continuation, String order, List<String> excludeTargets) {
|
||||
User user = currentUser();
|
||||
GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(rawStreamId);
|
||||
int limit = clampLimit(n);
|
||||
ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC;
|
||||
boolean unreadOnly = isUnreadOnly(excludeTargets);
|
||||
|
||||
PagedStatuses paged =
|
||||
fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||
PagedStatuses paged = fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly);
|
||||
|
||||
GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents();
|
||||
resp.setId(streamIdString(parsed));
|
||||
@@ -483,8 +463,7 @@ public class GoogleReaderREST {
|
||||
User user = currentUser();
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
Map<Long, FeedSubscription> subsByFeedId =
|
||||
subs.stream()
|
||||
.collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s, (a, b) -> a));
|
||||
subs.stream().collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s, (a, b) -> a));
|
||||
|
||||
List<GoogleReaderModel.StreamItem> items = new ArrayList<>();
|
||||
if (ids != null) {
|
||||
@@ -544,16 +523,13 @@ public class GoogleReaderREST {
|
||||
switch (streamId.type()) {
|
||||
case STARRED -> {
|
||||
List<FeedEntryStatus> starred =
|
||||
feedEntryStatusDAO.findStarred(
|
||||
user, null, null, offset, limit, order, true);
|
||||
feedEntryStatusDAO.findStarred(user, null, null, offset, limit, order, true);
|
||||
fetchedCount = starred.size();
|
||||
statuses =
|
||||
unreadOnly ? starred.stream().filter(s -> !s.isRead()).toList() : starred;
|
||||
statuses = unreadOnly ? starred.stream().filter(s -> !s.isRead()).toList() : starred;
|
||||
}
|
||||
case FEED -> {
|
||||
Long subId = streamId.feedSubscriptionId();
|
||||
FeedSubscription sub =
|
||||
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
FeedSubscription sub = subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
List<FeedSubscription> subs = sub == null ? List.of() : List.of(sub);
|
||||
statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
@@ -577,8 +553,7 @@ public class GoogleReaderREST {
|
||||
category == null
|
||||
? List.of()
|
||||
: feedSubscriptionDAO.findByCategories(
|
||||
user,
|
||||
feedCategoryDAO.findAllChildrenCategories(user, category));
|
||||
user, feedCategoryDAO.findAllChildrenCategories(user, category));
|
||||
statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
@@ -644,15 +619,12 @@ public class GoogleReaderREST {
|
||||
|
||||
if (StringUtils.isNotBlank(entry.getUrl())) {
|
||||
item.setCanonical(List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), null)));
|
||||
item.setAlternate(
|
||||
List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), "text/html")));
|
||||
item.setAlternate(List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), "text/html")));
|
||||
}
|
||||
item.setSummary(
|
||||
new GoogleReaderModel.Content(
|
||||
Optional.ofNullable(entry.getContent().getContent()).orElse(""),
|
||||
entry.getContent().getDirection() == FeedEntryContent.Direction.RTL
|
||||
? "rtl"
|
||||
: "ltr"));
|
||||
entry.getContent().getDirection() == FeedEntryContent.Direction.RTL ? "rtl" : "ltr"));
|
||||
|
||||
List<String> categories = new ArrayList<>();
|
||||
categories.add(GoogleReaderStreamId.READING_LIST);
|
||||
@@ -695,8 +667,7 @@ public class GoogleReaderREST {
|
||||
return s;
|
||||
}
|
||||
|
||||
private FeedCategory resolveLabelCategory(
|
||||
User user, String labelParam, boolean createIfMissing) {
|
||||
private FeedCategory resolveLabelCategory(User user, String labelParam, boolean createIfMissing) {
|
||||
if (StringUtils.isBlank(labelParam)) {
|
||||
return null;
|
||||
}
|
||||
@@ -717,10 +688,7 @@ public class GoogleReaderREST {
|
||||
}
|
||||
|
||||
private FeedCategory findCategoryByName(User user, String name) {
|
||||
return feedCategoryDAO.findAll(user).stream()
|
||||
.filter(c -> c.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return feedCategoryDAO.findAll(user).stream().filter(c -> c.getName().equals(name)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private String streamIdString(GoogleReaderStreamId parsed) {
|
||||
@@ -736,8 +704,7 @@ public class GoogleReaderREST {
|
||||
return switch (parsed.type()) {
|
||||
case FEED -> {
|
||||
Long subId = parsed.feedSubscriptionId();
|
||||
FeedSubscription sub =
|
||||
subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
FeedSubscription sub = subId == null ? null : feedSubscriptionDAO.findById(user, subId);
|
||||
yield sub == null ? null : sub.getTitle();
|
||||
}
|
||||
case LABEL -> parsed.value();
|
||||
@@ -747,8 +714,7 @@ public class GoogleReaderREST {
|
||||
}
|
||||
|
||||
private boolean isUnreadOnly(List<String> excludeTargets) {
|
||||
return excludeTargets != null
|
||||
&& excludeTargets.stream().anyMatch(t -> t.endsWith("/state/com.google/read"));
|
||||
return excludeTargets != null && excludeTargets.stream().anyMatch(t -> t.endsWith("/state/com.google/read"));
|
||||
}
|
||||
|
||||
private boolean containsTag(List<String> tags, String suffix) {
|
||||
|
||||
@@ -18,8 +18,7 @@ public record GoogleReaderStreamId(Type type, String value) {
|
||||
public static final String FEED_PREFIX = "feed/";
|
||||
|
||||
public static final GoogleReaderStreamId ALL_STREAM = new GoogleReaderStreamId(Type.ALL, null);
|
||||
public static final GoogleReaderStreamId STARRED_STREAM =
|
||||
new GoogleReaderStreamId(Type.STARRED, null);
|
||||
public static final GoogleReaderStreamId STARRED_STREAM = new GoogleReaderStreamId(Type.STARRED, null);
|
||||
|
||||
public static GoogleReaderStreamId parse(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "reading-list".equals(raw) || READING_LIST.equals(raw)) {
|
||||
|
||||
@@ -45,8 +45,7 @@ public class NextUnreadServlet {
|
||||
@Transactional
|
||||
@Operation(hidden = true)
|
||||
public Response get(
|
||||
@QueryParam("category") String categoryId,
|
||||
@QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
|
||||
@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return Response.temporaryRedirect(uri.getBaseUri()).build();
|
||||
@@ -62,24 +61,11 @@ public class NextUnreadServlet {
|
||||
} else {
|
||||
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
|
||||
if (category != null) {
|
||||
List<FeedCategory> children =
|
||||
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions =
|
||||
feedSubscriptionDAO.findByCategories(user, children);
|
||||
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
|
||||
List<FeedEntryStatus> statuses =
|
||||
feedEntryStatusDAO.findBySubscriptions(
|
||||
user,
|
||||
subscriptions,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
order,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
user, subscriptions, true, null, null, 0, 1, order, true, null, null, null);
|
||||
s = statuses.stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,7 @@ public class WebSocketSessions {
|
||||
public void sendMessage(User user, String text) {
|
||||
Set<Session> userSessions = sessions.get(user.getId());
|
||||
if (userSessions != null && !userSessions.isEmpty()) {
|
||||
log.debug(
|
||||
"sending '{}' to user {} via websocket ({} sessions)",
|
||||
text,
|
||||
user.getId(),
|
||||
userSessions.size());
|
||||
log.debug("sending '{}' to user {} via websocket ({} sessions)", text, user.getId(), userSessions.size());
|
||||
for (Session userSession : userSessions) {
|
||||
if (userSession.isOpen()) {
|
||||
userSession.getAsyncRemote().sendText(text);
|
||||
|
||||
@@ -18,10 +18,6 @@ public class CookieService {
|
||||
}
|
||||
|
||||
public NewCookie buildLogoutCookie() {
|
||||
return new NewCookie.Builder(cookieName)
|
||||
.maxAge(0)
|
||||
.expiry(Date.from(Instant.EPOCH))
|
||||
.path("/")
|
||||
.build();
|
||||
return new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class DatabaseApiKeyIdentityProvider
|
||||
implements IdentityProvider<TokenAuthenticationRequest> {
|
||||
public class DatabaseApiKeyIdentityProvider implements IdentityProvider<TokenAuthenticationRequest> {
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final UserService userService;
|
||||
@@ -40,8 +39,7 @@ public class DatabaseApiKeyIdentityProvider
|
||||
public Uni<SecurityIdentity> authenticate(
|
||||
TokenAuthenticationRequest request, AuthenticationRequestContext context) {
|
||||
return context.runBlocking(() -> {
|
||||
Optional<User> user =
|
||||
unitOfWork.call(() -> userService.login(request.getToken().getToken()));
|
||||
Optional<User> user = unitOfWork.call(() -> userService.login(request.getToken().getToken()));
|
||||
if (user.isEmpty()) {
|
||||
throw new AuthenticationFailedException("could not find a user with this api key");
|
||||
}
|
||||
|
||||
@@ -43,12 +43,9 @@ public class DatabaseUsernamePasswordIdentityProvider
|
||||
return context.runBlocking(() -> {
|
||||
Optional<User> user =
|
||||
unitOfWork.call(() ->
|
||||
userService.login(
|
||||
request.getUsername(),
|
||||
new String(request.getPassword().getPassword())));
|
||||
userService.login(request.getUsername(), new String(request.getPassword().getPassword())));
|
||||
if (user.isEmpty()) {
|
||||
throw new CommaFeedApplicationException(
|
||||
CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
|
||||
throw new CommaFeedApplicationException(CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
|
||||
}
|
||||
|
||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||
|
||||
@@ -19,8 +19,7 @@ import java.util.Set;
|
||||
public class ApiKeyAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||
|
||||
@Override
|
||||
public Uni<SecurityIdentity> authenticate(
|
||||
RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||
// only authorize api key for GET requests
|
||||
if (!context.request().method().name().equals("GET")) {
|
||||
return Uni.createFrom().optional(Optional.empty());
|
||||
|
||||
@@ -18,12 +18,11 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Authenticates requests to the Google Reader compatible API, using the "Authorization: GoogleLogin
|
||||
* auth=<token>" header set by Google Reader API clients.
|
||||
* Authenticates requests to the Google Reader compatible API, using the "Authorization: GoogleLogin auth=<token>"
|
||||
* header set by Google Reader API clients.
|
||||
*
|
||||
* <p>This mechanism only applies to requests under {@link #PATH_PREFIX}, so that a stray
|
||||
* "Authorization: GoogleLogin ..." header sent to any other endpoint is not mistakenly accepted as
|
||||
* an api key authentication.
|
||||
* <p>This mechanism only applies to requests under {@link #PATH_PREFIX}, so that a stray "Authorization: GoogleLogin
|
||||
* ..." header sent to any other endpoint is not mistakenly accepted as an api key authentication.
|
||||
*/
|
||||
@Singleton
|
||||
public class GoogleReaderAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||
@@ -34,8 +33,7 @@ public class GoogleReaderAuthenticationMechanism implements HttpAuthenticationMe
|
||||
private static final String AUTH_PREFIX = "GoogleLogin auth=";
|
||||
|
||||
@Override
|
||||
public Uni<SecurityIdentity> authenticate(
|
||||
RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
|
||||
if (!context.normalizedPath().startsWith(PATH_PREFIX)) {
|
||||
return Uni.createFrom().optional(Optional.empty());
|
||||
}
|
||||
|
||||
@@ -25,12 +25,10 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class generates an application.properties file with all the properties from {@link
|
||||
* CommaFeedConfiguration}.
|
||||
* This class generates an application.properties file with all the properties from {@link CommaFeedConfiguration}.
|
||||
*
|
||||
* <p>This is useful for people who want to be able to configure CommaFeed without having to look at
|
||||
* the code or the documentation, or for distribution packages that want to provide a default
|
||||
* configuration file.
|
||||
* <p>This is useful for people who want to be able to configure CommaFeed without having to look at the code or the
|
||||
* documentation, or for distribution packages that want to provide a default configuration file.
|
||||
*/
|
||||
public class CommaFeedPropertiesGenerator {
|
||||
|
||||
@@ -41,8 +39,7 @@ public class CommaFeedPropertiesGenerator {
|
||||
|
||||
Path modelPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL);
|
||||
Path javadocPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC);
|
||||
Path outputPath =
|
||||
targetPath.resolve("quarkus-generated-doc").resolve("application.properties");
|
||||
Path outputPath = targetPath.resolve("quarkus-generated-doc").resolve("application.properties");
|
||||
|
||||
try (InputStream model = Files.newInputStream(modelPath);
|
||||
InputStream javadoc = Files.newInputStream(javadocPath);
|
||||
@@ -52,10 +49,8 @@ public class CommaFeedPropertiesGenerator {
|
||||
}
|
||||
|
||||
void generate(InputStream model, InputStream javadoc, OutputStream output) throws IOException {
|
||||
ResolvedModel resolvedModel =
|
||||
JacksonMappers.yamlObjectReader().readValue(model, ResolvedModel.class);
|
||||
JavadocElements javadocElements =
|
||||
JacksonMappers.yamlObjectReader().readValue(javadoc, JavadocElements.class);
|
||||
ResolvedModel resolvedModel = JacksonMappers.yamlObjectReader().readValue(model, ResolvedModel.class);
|
||||
JavadocElements javadocElements = JacksonMappers.yamlObjectReader().readValue(javadoc, JavadocElements.class);
|
||||
|
||||
for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) {
|
||||
for (AbstractConfigItem item : configRoot.getItems()) {
|
||||
@@ -66,8 +61,7 @@ public class CommaFeedPropertiesGenerator {
|
||||
IOUtils.write(String.join("\n", lines), output, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void handleAbstractConfigItem(
|
||||
AbstractConfigItem item, JavadocElements javadocElements) {
|
||||
private void handleAbstractConfigItem(AbstractConfigItem item, JavadocElements javadocElements) {
|
||||
if (item.isSection()) {
|
||||
handleSection((ConfigSection) item, javadocElements);
|
||||
} else {
|
||||
@@ -89,8 +83,7 @@ public class CommaFeedPropertiesGenerator {
|
||||
.get(property.getSourceType() + "." + property.getSourceElementName())
|
||||
.description()
|
||||
.replace("\n", "\n# ");
|
||||
String defaultValue =
|
||||
Optional.ofNullable(property.getDefaultValue()).orElse("").toLowerCase();
|
||||
String defaultValue = Optional.ofNullable(property.getDefaultValue()).orElse("").toLowerCase();
|
||||
|
||||
lines.add("# " + description);
|
||||
lines.add(key + "=" + defaultValue);
|
||||
|
||||
Reference in New Issue
Block a user