Skip to content

Commit

Permalink
feat: restructure RSS generation to support extension by other plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Nov 28, 2024
1 parent 857696c commit d573697
Show file tree
Hide file tree
Showing 37 changed files with 1,127 additions and 792 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ jobs:
with:
app-id: app-KhIVw
skip-node-setup: true
artifacts-path: 'app/build/libs'
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jobs:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v1
with:
skip-node-setup: true
artifacts-path: 'app/build/libs'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ application-local.properties

/admin-frontend/node_modules/
/workplace/
*/workplace/
73 changes: 73 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
plugins {
id 'java-library'
id 'maven-publish'
id "io.freefair.lombok" version "8.0.0-rc2"
}

group = 'run.halo.feed'
version = rootProject.version

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"
javadoc.options.encoding = "UTF-8"

dependencies {
api platform('run.halo.tools.platform:plugin:2.20.0-SNAPSHOT')
compileOnly 'run.halo.app:api'
}

test {
useJUnitPlatform()
}

publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifactId = 'api'
version = project.hasProperty('version') ? project.property('version') : 'unspecified'

pom {
name = 'RSS'
description = '为站点生成 RSS 订阅链接'
url = 'https://www.halo.run/store/apps/app-KhIVw'

licenses {
license {
name = 'GPL-3.0'
url = 'https://github.com/halo-dev/plugin-feed/blob/main/LICENSE'
}
}

developers {
developer {
id = 'guqing'
name = 'guqing'
email = '[email protected]'
}
}

scm {
connection = 'scm:git:[email protected]:halo-dev/plugin-feed.git'
developerConnection = 'scm:git:[email protected]:halo-dev/plugin-feed.git'
url = 'https://github.com/halo-dev/plugin-feed'
}
}
}
}
repositories {
maven {
url = version.endsWith('-SNAPSHOT') ? 'https://s01.oss.sonatype.org/content/repositories/snapshots/' :
'https://s01.oss.sonatype.org/content/repositories/releases/'
credentials {
username = project.findProperty("ossr.user") ?: System.getenv("OSSR_USERNAME")
password = project.findProperty("ossr.password") ?: System.getenv("OSSR_PASSWORD")
}
}
}
}
105 changes: 105 additions & 0 deletions api/src/main/java/run/halo/feed/RSS2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package run.halo.feed;

import jakarta.validation.constraints.NotBlank;
import java.time.Instant;
import java.util.List;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class RSS2 {
/**
* (Recommended) The name of the feed, which should be plain text only
*/
@NotBlank
private String title;

/**
* (Recommended) The URL of the website associated with the feed, which should link to a
* human-readable website
*/
@NotBlank
private String link;

/**
* (Optional) The summary of the feed, which should be plain text only
*/
private String description;

/**
* The primary language of the feed, which should be a value from
* <a href="https://www.rssboard.org/rss-language-codes">RSS Language Codes</a> or ISO
* 639 language codes
*/
private String language;

/**
* (Recommended) The URL of the image that represents the channel, which should be relatively
* large and square
*/
private String image;

private List<Item> items;

@Data
@Builder
public static class Item {
/**
* (Required) The title of the item, which should be plain text only
*/
@NotBlank
private String title;

/**
* (Recommended) The URL of the item, which should link to a human-readable website
*/
@NotBlank
private String link;

/**
* <p>(Recommended) The content of the item. For an Atom feed, it's the atom:content
* element.</p>
* <p>For a JSON feed, it's the content_html field.</p>
*/
@NotBlank
private String description;

/**
* (Optional) The author of the item
*/
private String author;

/**
* (Optional) The category of the item. You can use a plain string or an array of strings
*/
private List<String> categories;

/**
* (Recommended) The publication
* <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date">date </a> of the item, which should be a Date object
* following <a href="https://docs.rsshub.app/joinus/advanced/pub-date">the standard</a>
*/
private Instant pubDate;

/**
* (Optional) The unique identifier of the item
*/
private String guid;

/**
* (Optional) The URL of an enclosure associated with the item
*/
private String enclosureUrl;

/**
* (Optional) The size of the enclosure file in byte, which should be a number
*/
private String enclosureLength;

/**
* (Optional) The MIME type of the enclosure file, which should be a string
*/
private String enclosureType;
}
}
36 changes: 36 additions & 0 deletions api/src/main/java/run/halo/feed/RssRouteItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package run.halo.feed;

import org.pf4j.ExtensionPoint;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Mono;

public interface RssRouteItem extends ExtensionPoint {

@NonNull
String pathPattern();

@NonNull
String displayName();

default String description() {
return "";
}

/**
* An example URI for this route.
*/
default String example() {
return "";
}

/**
* The namespace of this route to avoid conflicts.
*/
default String namespace() {
return "";
}

@NonNull
Mono<RSS2> handler(ServerRequest request);
}
30 changes: 30 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
id 'java'
id "io.freefair.lombok" version "8.0.0-rc2"
id "run.halo.plugin.devtools" version "0.4.1"
}

group = 'run.halo.feed'
version = rootProject.version

dependencies {
implementation project(':api')
compileOnly 'run.halo.app:api'
implementation 'org.dom4j:dom4j:2.1.3'
implementation('org.apache.commons:commons-text:1.10.0') {
// Because the transitive dependency already exists in halo.
exclude group: 'org.apache.commons', module: 'commons-lang3'
}

testImplementation 'run.halo.app:api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
useJUnitPlatform()
}

halo {
version = '2.20.10'
debug = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public class BasicSetting {

private Integer outputNum = 20;


enum DescriptionType {
/**
* 全文
Expand Down
File renamed without changes.
108 changes: 108 additions & 0 deletions app/src/main/java/run/halo/feed/FeedPluginEndpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package run.halo.feed;

import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;

import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.feed.provider.PostRssProvider;

@Component
@AllArgsConstructor
public class FeedPluginEndpoint {
static final RequestPredicate requestPredicate = accept(
MediaType.TEXT_XML,
MediaType.APPLICATION_RSS_XML
);

private final PostRssProvider postRssProvider;
private final ExtensionGetter extensionGetter;
private final RssCacheManager rssCacheManager;

@Bean
RouterFunction<ServerResponse> rssRouterFunction() {
return RouterFunctions.route()
.GET(path("/feed.xml").or(path("/rss.xml")).and(requestPredicate),
request -> rssCacheManager.get("rss.xml", postRssProvider.handler(request))
.flatMap(this::buildResponse)
)
.build();
}

@Bean
RouterFunction<ServerResponse> additionalRssRouter() {
var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/feed/**");
return new RouterFunction<>() {
@Override
@NonNull
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
return pathMatcher.matches(request.exchange())
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap(matched -> handleRequest(request));
}

private Mono<HandlerFunction<ServerResponse>> handleRequest(ServerRequest request) {
return extensionGetter.getEnabledExtensions(RssRouteItem.class)
.filter(item -> StringUtils.isNotBlank(item.pathPattern()))
.map(routeItem -> new RouteItem(buildRequestPredicate(routeItem),
buildHandleFunction(routeItem))
)
.filter(route -> route.requestPredicate.test(request))
.next()
.map(RouteItem::handler);
}

record RouteItem(RequestPredicate requestPredicate,
HandlerFunction<ServerResponse> handler) {
}

private HandlerFunction<ServerResponse> buildHandleFunction(RssRouteItem routeItem) {
return request -> rssCacheManager.get(request.path(), routeItem.handler(request))
.flatMap(item -> buildResponse(item));
}

private RequestPredicate buildRequestPredicate(RssRouteItem routeItem) {
return path(buildPathPattern(routeItem)).and(requestPredicate);
}

private String buildPathPattern(RssRouteItem routeItem) {
var sb = new StringBuilder("/feed/");

if (StringUtils.isNotBlank(routeItem.namespace())) {
sb.append(routeItem.namespace());
if (!routeItem.namespace().endsWith("/")) {
sb.append("/");
}
}

String pathPattern = routeItem.pathPattern();
if (pathPattern.startsWith("/")) {
pathPattern = pathPattern.substring(1);
}
sb.append(pathPattern);

return sb.toString();
}
};
}

private Mono<ServerResponse> buildResponse(String xml) {
return ServerResponse.ok().contentType(MediaType.TEXT_XML)
.bodyValue(xml);
}
}
Loading

0 comments on commit d573697

Please sign in to comment.