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 Dec 5, 2024
1 parent 857696c commit 8646bac
Show file tree
Hide file tree
Showing 40 changed files with 1,448 additions and 825 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.11-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")
}
}
}
}
40 changes: 40 additions & 0 deletions api/src/main/java/run/halo/feed/CacheClearRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package run.halo.feed;

import org.springframework.util.Assert;

public record CacheClearRule(Type type, String value) {
public CacheClearRule {
Assert.notNull(type, "Type cannot be null");
Assert.notNull(value, "Value cannot be null");
if (type == Type.EXACT && !value.startsWith("/")) {
throw new IllegalArgumentException("Exact value must start with /");
}
}

public static CacheClearRule forPrefix(String prefix) {
return new CacheClearRule(Type.PREFIX, prefix);
}

public static CacheClearRule forExact(String exact) {
return new CacheClearRule(Type.EXACT, exact);
}

public static CacheClearRule forContains(String contains) {
return new CacheClearRule(Type.CONTAINS, contains);
}

@Override
public String toString() {
return "CacheClearRule{" +
"type=" + type +
", value='" + value + '\'' +
'}';
}

public enum Type {
PREFIX,
EXACT,
CONTAINS
}
}

186 changes: 186 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,186 @@
package run.halo.feed;

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

@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;

@Singular
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
*/
@Singular
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;

/**
* (Optional) Media content, represented by the <media:content> element.
*/
@Singular
private List<MediaContent> mediaContents;
}

@Data
@Builder
public static class MediaContent {
/**
* URL of the media object.
*/
private String url;

/**
* Type of the media, such as "image/jpeg", "audio/mpeg", "video/mp4".
*/
private String type;

/**
* The general type of media: image, audio, video.
*/
private MediaType mediaType;

/**
* File size of the media object in bytes.
*/
private String fileSize;

/**
* Duration of the media object in seconds (for audio and video).
*/
private String duration;

/**
* Height of the media object in pixels (for image and video).
*/
private String height;

/**
* Width of the media object in pixels (for image and video).
*/
private String width;

/**
* Bitrate of the media (for audio and video).
*/
private String bitrate;

/**
* Thumbnail associated with this media content.
*/
private MediaThumbnail thumbnail;

public enum MediaType {
IMAGE, AUDIO, VIDEO, DOCUMENT
}
}

@Data
@Builder
public static class MediaThumbnail {
/**
* URL of the thumbnail.
*/
private String url;

/**
* Height of the thumbnail in pixels.
*/
private String height;

/**
* Width of the thumbnail in pixels.
*/
private String width;
}
}
68 changes: 68 additions & 0 deletions api/src/main/java/run/halo/feed/RssCacheClearRequested.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package run.halo.feed;

import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import run.halo.app.plugin.SharedEvent;

/**
* Represents an event to request the clearing of RSS cache with flexible rules.
* This event allows specifying multiple strategies for cache invalidation, including
* prefix matching, exact route matching, and keyword containment.
*
* <p>Attributes:</p>
* <ul>
* <li>rules (required, List):
* A list of rules defining the cache clearing strategy. Each rule includes:
* <ul>
* <li>type (required, String): The type of matching rule. Supported values are:
* <ul>
* <li>prefix: Matches cache entries with keys that start with the specified prefix.</li>
* <li>exact: Matches cache entries with keys that exactly match the specified value.</li>
* <li>contains: Matches cache entries with keys that contain the specified substring
* .</li>
* </ul>
* </li>
* <li>value (required, String): The matching value for the rule.
* <ul>
* <li>For type "prefix", the value is the prefix path (e.g., "/feed/").</li>
* <li>For type "exact", the value is the exact route (e.g., "/feed/moments/rss.xml")
* .</li>
* <li>For type "contains", the value is a substring to search for (e.g., "moments").</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>applyToAll (optional, boolean, default: false):
* Indicates whether to clear all cache entries. If true, all rules in the "rules" list are
* ignored,
* and the entire cache is cleared.</li>
* </ul>
*/
@SharedEvent
@Getter
public class RssCacheClearRequested extends ApplicationEvent {
private final List<CacheClearRule> rules;
private final boolean applyToAll;

public RssCacheClearRequested(Object source, List<CacheClearRule> rules, boolean applyToAll) {
super(source);
this.rules = (rules == null ? List.of() : new ArrayList<>(rules));
this.applyToAll = applyToAll;
}

public static RssCacheClearRequested forAll(Object source) {
return new RssCacheClearRequested(source, null, true);
}

public static RssCacheClearRequested forRules(Object source, List<CacheClearRule> rules) {
return new RssCacheClearRequested(source, rules, false);
}

public static RssCacheClearRequested forRule(Object source, CacheClearRule rule) {
List<CacheClearRule> rules = new ArrayList<>();
rules.add(rule);
return new RssCacheClearRequested(source, rules, false);
}
}
Loading

0 comments on commit 8646bac

Please sign in to comment.