diff --git a/build.gradle b/build.gradle index 9919a97..04356a2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id "com.github.node-gradle.node" version "5.0.0" id "io.freefair.lombok" version "8.0.0-rc2" - id "run.halo.plugin.devtools" version "0.1.1" + id "run.halo.plugin.devtools" version "0.4.1" id 'java' } @@ -15,8 +15,9 @@ repositories { } dependencies { - implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT') + implementation platform('run.halo.tools.platform:plugin:2.20.11-SNAPSHOT') compileOnly 'run.halo.app:api' + compileOnly "run.halo.feed:api:0.0.1-SNAPSHOT" testImplementation 'run.halo.app:api' testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -46,11 +47,10 @@ build { } halo { - version = '2.17.0' + version = '2.20.11' debug = true; } - haloPlugin { openApi { outputDir = file("$rootDir/api-docs/openapi/v3_0") @@ -58,9 +58,9 @@ haloPlugin { momentsApi { displayName = 'Extension API for Moments Plugin' pathsToMatch = [ - '/apis/moment.halo.run/v1alpha1/**', - '/apis/console.api.moment.halo.run/v1alpha1/**', - '/apis/uc.api.moment.halo.run/v1alpha1/**' + '/apis/moment.halo.run/v1alpha1/**', + '/apis/console.api.moment.halo.run/v1alpha1/**', + '/apis/uc.api.moment.halo.run/v1alpha1/**' ] } } diff --git a/src/main/java/run/halo/moments/CommentNotificationReasonPublisher.java b/src/main/java/run/halo/moments/CommentNotificationReasonPublisher.java index d5d5f4d..86fb3ef 100644 --- a/src/main/java/run/halo/moments/CommentNotificationReasonPublisher.java +++ b/src/main/java/run/halo/moments/CommentNotificationReasonPublisher.java @@ -24,6 +24,7 @@ import run.halo.app.infra.utils.JsonUtils; import run.halo.app.notification.NotificationReasonEmitter; import run.halo.app.notification.UserIdentity; +import run.halo.moments.event.MomentHasNewCommentEvent; /** * Notification reason publisher for {@link Comment}. diff --git a/src/main/java/run/halo/moments/CommentReconciler.java b/src/main/java/run/halo/moments/CommentReconciler.java index 92e6867..36ec568 100644 --- a/src/main/java/run/halo/moments/CommentReconciler.java +++ b/src/main/java/run/halo/moments/CommentReconciler.java @@ -12,6 +12,7 @@ import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; +import run.halo.moments.event.MomentHasNewCommentEvent; /** * Reconciler for comment. diff --git a/src/main/java/run/halo/moments/MomentReconciler.java b/src/main/java/run/halo/moments/MomentReconciler.java index e939dc7..a965a96 100644 --- a/src/main/java/run/halo/moments/MomentReconciler.java +++ b/src/main/java/run/halo/moments/MomentReconciler.java @@ -5,6 +5,7 @@ import java.time.Instant; import java.util.Set; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import run.halo.app.core.extension.notification.Subscription; import run.halo.app.extension.DefaultExtensionMatcher; @@ -15,6 +16,8 @@ import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.notification.NotificationCenter; +import run.halo.moments.event.MomentDeletedEvent; +import run.halo.moments.event.MomentUpdatedEvent; /** * {@link Reconciler} for {@link Moment}. @@ -29,6 +32,7 @@ public class MomentReconciler implements Reconciler { private static final String FINALIZER = "moment-protection"; private final ExtensionClient client; private final NotificationCenter notificationCenter; + private final ApplicationEventPublisher eventPublisher; @Override public Result reconcile(Request request) { @@ -36,6 +40,7 @@ public Result reconcile(Request request) { if (ExtensionUtil.isDeleted(moment)) { if (ExtensionUtil.removeFinalizers(moment.getMetadata(), Set.of(FINALIZER))) { client.update(moment); + eventPublisher.publishEvent(new MomentDeletedEvent(this, request.name())); } return; } @@ -57,6 +62,8 @@ public Result reconcile(Request request) { moment.getSpec().setApprovedTime(Instant.now()); } client.update(moment); + + eventPublisher.publishEvent(new MomentUpdatedEvent(this, request.name())); }); return Result.doNotRetry(); } diff --git a/src/main/java/run/halo/moments/MomentRouter.java b/src/main/java/run/halo/moments/MomentRouter.java index a137833..33c08db 100644 --- a/src/main/java/run/halo/moments/MomentRouter.java +++ b/src/main/java/run/halo/moments/MomentRouter.java @@ -4,15 +4,12 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.route; import static run.halo.app.theme.router.PageUrlUtils.totalPage; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.springframework.context.annotation.Bean; -import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RouterFunction; @@ -20,16 +17,10 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; -import run.halo.app.extension.ConfigMap; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.infra.SystemSetting; -import run.halo.app.infra.utils.JsonUtils; import run.halo.app.plugin.ReactiveSettingFetcher; import run.halo.app.theme.router.PageUrlUtils; import run.halo.app.theme.router.UrlContextListResult; import run.halo.moments.finders.MomentFinder; -import run.halo.moments.util.RSS2; import run.halo.moments.vo.MomentVo; @@ -51,69 +42,12 @@ public class MomentRouter { private final ReactiveSettingFetcher settingFetcher; - private final ReactiveExtensionClient client; - - private final ExternalUrlSupplier externalUrlSupplier; - @Bean RouterFunction momentRouterFunction() { return route(GET("/moments").or(GET("/moments/page/{page:\\d+}")), handlerFunction()) - .andRoute(GET("/moments/rss.xml"), handlerRss()) .andRoute(GET("/moments/{momentName:\\S+}"), handlerMomentDefault()); } - private HandlerFunction handlerRss() { - return request -> ServerResponse.ok() - .contentType(MediaType.TEXT_XML) - .body(buildRss(request), String.class); - } - - private Mono buildRss(ServerRequest request) { - var externalUrl = externalUrlSupplier.get(); - if (!externalUrl.isAbsolute()) { - externalUrl = request.exchange().getRequest().getURI().resolve(externalUrl); - } - - final var hostAddress = externalUrl; - return getSystemBasicSetting() - .flatMap(basicSetting -> getMomentTitle() - .map(momentTitle -> RSS2.builder() - .title(StringUtils.defaultString(basicSetting.getTitle()) + momentTitle) - .link(StringUtils.removeEnd(hostAddress.toString(), "/")) - .description(StringUtils.defaultString(basicSetting.getSubtitle())) - ) - ) - .flatMap(builder -> momentFinder.listAll() - .map(momentVo -> RSS2.Item.builder() - .title(momentVo.getOwner().getDisplayName() + " published on " - + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()) - .format(momentVo.getSpec().getReleaseTime())) - .link(hostAddress.resolve("moments/" + momentVo.getMetadata().getName()) - .toString()) - .guid(hostAddress.resolve("moments/" + momentVo.getMetadata().getName()) - .toString()) - .description(""" - - """.formatted(momentVo.getSpec().getContent().getHtml())) - .pubDate(momentVo.getSpec().getReleaseTime()).build()) - .collectList() - .map(builder::items) - ) - .map(RSS2.RSS2Builder::build) - .map(RSS2::toXmlString); - } - - private Mono getSystemBasicSetting() { - return client.get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG) - .mapNotNull(ConfigMap::getData) - .map(map -> { - String basicSetting = map.getOrDefault(SystemSetting.Basic.GROUP, "{}"); - return JsonUtils.jsonToObject(basicSetting, SystemSetting.Basic.class); - }); - } - - private HandlerFunction handlerMomentDefault() { return request -> { String momentName = request.pathVariable("momentName"); diff --git a/src/main/java/run/halo/moments/event/MomentDeletedEvent.java b/src/main/java/run/halo/moments/event/MomentDeletedEvent.java new file mode 100644 index 0000000..608b27b --- /dev/null +++ b/src/main/java/run/halo/moments/event/MomentDeletedEvent.java @@ -0,0 +1,14 @@ +package run.halo.moments.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MomentDeletedEvent extends ApplicationEvent { + private final String momentName; + + public MomentDeletedEvent(Object source, String momentName) { + super(source); + this.momentName = momentName; + } +} diff --git a/src/main/java/run/halo/moments/MomentHasNewCommentEvent.java b/src/main/java/run/halo/moments/event/MomentHasNewCommentEvent.java similarity index 93% rename from src/main/java/run/halo/moments/MomentHasNewCommentEvent.java rename to src/main/java/run/halo/moments/event/MomentHasNewCommentEvent.java index f100328..1a7adcf 100644 --- a/src/main/java/run/halo/moments/MomentHasNewCommentEvent.java +++ b/src/main/java/run/halo/moments/event/MomentHasNewCommentEvent.java @@ -1,4 +1,4 @@ -package run.halo.moments; +package run.halo.moments.event; import lombok.Getter; import org.springframework.context.ApplicationEvent; diff --git a/src/main/java/run/halo/moments/event/MomentUpdatedEvent.java b/src/main/java/run/halo/moments/event/MomentUpdatedEvent.java new file mode 100644 index 0000000..35fa833 --- /dev/null +++ b/src/main/java/run/halo/moments/event/MomentUpdatedEvent.java @@ -0,0 +1,14 @@ +package run.halo.moments.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MomentUpdatedEvent extends ApplicationEvent { + private final String momentName; + + public MomentUpdatedEvent(Object source, String momentName) { + super(source); + this.momentName = momentName; + } +} diff --git a/src/main/java/run/halo/moments/finders/impl/MomentFinderImpl.java b/src/main/java/run/halo/moments/finders/impl/MomentFinderImpl.java index 2c3e8de..e624586 100644 --- a/src/main/java/run/halo/moments/finders/impl/MomentFinderImpl.java +++ b/src/main/java/run/halo/moments/finders/impl/MomentFinderImpl.java @@ -16,6 +16,7 @@ import reactor.core.publisher.Mono; import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.User; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; import run.halo.app.extension.PageRequest; @@ -59,7 +60,7 @@ public Flux listAll() { listOptions.setFieldSelector( FieldSelector.of(FIXED_QUERY)); return client.listAll(Moment.class, listOptions, defaultSort()) - .flatMap(this::getMomentVo); + .concatMap(this::getMomentVo); } @Override @@ -69,7 +70,8 @@ public Mono> list(Integer page, Integer size) { } static Sort defaultSort() { - return Sort.by("spec.releaseTime").descending(); + return Sort.by("spec.releaseTime").descending() + .and(ExtensionUtil.defaultSort()); } @Override @@ -78,7 +80,7 @@ public Flux listBy(String tag) { var query = and(FIXED_QUERY, equal("spec.tags", tag)); listOptions.setFieldSelector(FieldSelector.of(query)); return client.listAll(Moment.class, listOptions, defaultSort()) - .flatMap(this::getMomentVo); + .concatMap(this::getMomentVo); } @Override @@ -104,7 +106,7 @@ public Flux listAllTags() { .toList(); }) .groupBy(MomentTagPair::tagName) - .flatMap(groupedFlux -> groupedFlux.count() + .concatMap(groupedFlux -> groupedFlux.count() .defaultIfEmpty(0L) .map(count -> MomentTagVo.builder() .name(groupedFlux.key()) diff --git a/src/main/java/run/halo/moments/rss/MomentRssProvider.java b/src/main/java/run/halo/moments/rss/MomentRssProvider.java new file mode 100644 index 0000000..0faeee6 --- /dev/null +++ b/src/main/java/run/halo/moments/rss/MomentRssProvider.java @@ -0,0 +1,200 @@ +package run.halo.moments.rss; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.springframework.lang.NonNull; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.server.ServerRequest; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import run.halo.app.core.attachment.ThumbnailSize; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.ExternalLinkProcessor; +import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemInfoGetter; +import run.halo.app.plugin.ReactiveSettingFetcher; +import run.halo.feed.RSS2; +import run.halo.feed.RssRouteItem; +import run.halo.moments.Moment; +import run.halo.moments.finders.MomentFinder; +import run.halo.moments.vo.MomentVo; + +@RequiredArgsConstructor +public class MomentRssProvider implements RssRouteItem { + private final ExternalUrlSupplier externalUrlSupplier; + private final ExternalLinkProcessor externalLinkProcessor; + private final ReactiveExtensionClient client; + private final ReactiveSettingFetcher settingFetcher; + private final MomentFinder momentFinder; + private final SystemInfoGetter systemInfoGetter; + + @Override + public Mono pathPattern() { + return Mono.fromSupplier(() -> "/moments/rss.xml"); + } + + @Override + @NonNull + public String displayName() { + return "瞬间"; + } + + @Override + public String description() { + return "瞬间 RSS"; + } + + @Override + public String example() { + return "https://example.com/feed/moments/rss.xml"; + } + + @Override + public Mono handler(ServerRequest request) { + return buildRss(request); + } + + private Mono buildRss(ServerRequest request) { + var externalUrl = externalUrlSupplier.getURL(request.exchange().getRequest()); + + var builder = RSS2.builder(); + var rssMono = systemInfoGetter.get() + .flatMap(info -> getMomentPageTitle() + .doOnNext(momentTitle -> info.setTitle( + String.join(" | ", info.getTitle(), momentTitle)) + ) + .thenReturn(info) + ) + .map(basic -> builder + .title(basic.getTitle()) + .image(externalLinkProcessor.processLink(basic.getLogo())) + .description(StringUtils.defaultIfBlank(basic.getSubtitle(), + basic.getTitle())) + .link(externalUrl.toString()) + ) + .subscribeOn(Schedulers.boundedElastic()); + + var rssItemMono = momentFinder.listAll() + .map(moment -> { + var permalink = getMomentPermalink(moment); + var medium = moment.getSpec().getContent().getMedium(); + var mediumHtml = generateMediaHtmlList(medium); + var htmlContent = processHtml(moment.getSpec().getContent().getHtml()); + return RSS2.Item.builder() + .title(buildMomentTitle(moment)) + .link(externalLinkProcessor.processLink(permalink)) + .pubDate(moment.getSpec().getReleaseTime()) + .guid(permalink) + .description(htmlContent + mediumHtml) + .build(); + }) + .collectList() + .doOnNext(builder::items) + .subscribeOn(Schedulers.boundedElastic()); + return Mono.when(rssMono, rssItemMono) + .then(Mono.fromSupplier(builder::build)); + } + + private String processHtml(String html) { + var document = Jsoup.parse(html); + + // Process all links + var links = document.select("a[href]"); + for (Element link : links) { + var isTag = link.hasClass("tag"); + String href = link.attr("href"); + if (isTag && href.startsWith("?")) { + // 兼容旧版标签链接 + href = "/moments" + href; + } + var absoluteUrl = externalLinkProcessor.processLink(href); + link.attr("href", absoluteUrl); + } + // process all images + var images = document.select("img[src]"); + for (Element image : images) { + String src = image.attr("src"); + var thumb = genThumbUrl(src, ThumbnailSize.M); + var absoluteUrl = externalLinkProcessor.processLink(thumb); + image.attr("src", absoluteUrl); + } + return document.body().html(); + } + + private String genThumbUrl(String url, ThumbnailSize size) { + return externalLinkProcessor.processLink( + "/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=" + url + "&size=" + + size.name().toLowerCase() + ); + } + + private String generateMediaHtmlList(List medium) { + if (CollectionUtils.isEmpty(medium)) { + return ""; + } + return medium.stream() + .map(this::generateSingleMediaHtml) + .collect(Collectors.joining()); + } + + private String generateSingleMediaHtml(Moment.MomentMedia media) { + var url = media.getUrl(); + return switch (media.getType()) { + case PHOTO -> generatePhotoHtml(media); + case VIDEO -> + String.format("", + url); + case AUDIO -> + String.format("", + url); + case POST -> String.format("%s", url, url); + }; + } + + private String generatePhotoHtml(Moment.MomentMedia media) { + // the best practice is to use the thumbnail for src + var mSrc = genThumbUrl(media.getUrl(), ThumbnailSize.M); + // If the reader does not support srcset, then only src, + var srcSet = """ + %s 400w, + %s 800w, + %s 1200w, + """.formatted( + genThumbUrl(media.getUrl(), ThumbnailSize.S), + mSrc, + genThumbUrl(media.getUrl(), ThumbnailSize.L) + ); + return String.format( + "\"moment", + mSrc, + srcSet + ); + } + + private static List nullSafeList(List list) { + return list == null ? List.of() : list; + } + + private static String getMomentPermalink(MomentVo moment) { + return "moments/" + moment.getMetadata().getName(); + } + + private static String buildMomentTitle(MomentVo momentVo) { + return momentVo.getOwner().getDisplayName() + " published on " + + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(ZoneId.systemDefault()) + .format(momentVo.getSpec().getReleaseTime()); + } + + Mono getMomentPageTitle() { + return this.settingFetcher.get("base") + .map(setting -> setting.get("title").asText("瞬间")) + .defaultIfEmpty("瞬间"); + } +} diff --git a/src/main/java/run/halo/moments/rss/OldRssRouteRedirectionFilter.java b/src/main/java/run/halo/moments/rss/OldRssRouteRedirectionFilter.java new file mode 100644 index 0000000..e3a88c4 --- /dev/null +++ b/src/main/java/run/halo/moments/rss/OldRssRouteRedirectionFilter.java @@ -0,0 +1,38 @@ +package run.halo.moments.rss; + +import java.net.URI; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.lang.NonNull; +import org.springframework.security.web.server.DefaultServerRedirectStrategy; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import run.halo.app.security.AdditionalWebFilter; + +/** + *

The detail page address of the moment is also /moments/{name}, so you need to + * use a filter for redirection instead of directly using the route.

+ */ +public class OldRssRouteRedirectionFilter implements AdditionalWebFilter { + private final DefaultServerRedirectStrategy redirectStrategy = + new DefaultServerRedirectStrategy(); + private final ServerWebExchangeMatcher requestMatcher = ServerWebExchangeMatchers.pathMatchers( + HttpMethod.GET, "/moments/rss.xml"); + + @Override + @NonNull + public Mono filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { + return requestMatcher.matches(exchange) + .flatMap(matchResult -> { + if (matchResult.isMatch()) { + redirectStrategy.setHttpStatus(HttpStatus.PERMANENT_REDIRECT); + return redirectStrategy + .sendRedirect(exchange, URI.create("/feed/moments/rss.xml")); + } + return chain.filter(exchange); + }); + } +} diff --git a/src/main/java/run/halo/moments/rss/RssAutoConfiguration.java b/src/main/java/run/halo/moments/rss/RssAutoConfiguration.java new file mode 100644 index 0000000..6375f98 --- /dev/null +++ b/src/main/java/run/halo/moments/rss/RssAutoConfiguration.java @@ -0,0 +1,53 @@ +package run.halo.moments.rss; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.ExternalLinkProcessor; +import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemInfoGetter; +import run.halo.app.plugin.ReactiveSettingFetcher; +import run.halo.app.security.AdditionalWebFilter; +import run.halo.feed.CacheClearRule; +import run.halo.feed.RssCacheClearRequested; +import run.halo.moments.event.MomentDeletedEvent; +import run.halo.moments.event.MomentUpdatedEvent; +import run.halo.moments.finders.MomentFinder; + +@Configuration +@ConditionalOnClass(name = "run.halo.feed.RssRouteItem") +@RequiredArgsConstructor +public class RssAutoConfiguration { + private final ExternalUrlSupplier externalUrlSupplier; + private final ExternalLinkProcessor externalLinkProcessor; + private final ReactiveExtensionClient client; + private final ReactiveSettingFetcher settingFetcher; + private final MomentFinder momentFinder; + private final SystemInfoGetter systemInfoGetter; + private final ApplicationEventPublisher eventPublisher; + + @Bean + MomentRssProvider momentRssProvider() { + return new MomentRssProvider(externalUrlSupplier, externalLinkProcessor, client, + settingFetcher, momentFinder, systemInfoGetter); + } + + @Async + @EventListener({MomentUpdatedEvent.class, MomentDeletedEvent.class, ContextClosedEvent.class}) + public void onMomentUpdatedOrDeleted() { + var rule = CacheClearRule.forExact("/feed/moments/rss.xml"); + var event = RssCacheClearRequested.forRule(this, rule); + eventPublisher.publishEvent(event); + } + + @Bean + AdditionalWebFilter oldRssRedirectWebFilter() { + return new OldRssRouteRedirectionFilter(); + } +} diff --git a/src/main/java/run/halo/moments/util/RSS2.java b/src/main/java/run/halo/moments/util/RSS2.java deleted file mode 100644 index 76ed8bc..0000000 --- a/src/main/java/run/halo/moments/util/RSS2.java +++ /dev/null @@ -1,88 +0,0 @@ -package run.halo.moments.util; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Properties; -import java.util.stream.Collectors; -import lombok.Builder; -import lombok.Data; -import org.springframework.util.PropertyPlaceholderHelper; - -@Data -@Builder -public class RSS2 { - private static final PropertyPlaceholderHelper PLACEHOLDER_HELPER = new PropertyPlaceholderHelper("${", "}"); - private String title; - - private String link; - - private String description; - - private List items; - - @Data - @Builder - public static class Item { - private String title; - - private String link; - - private String description; - - private Instant pubDate; - - private String guid; - } - - public String toXmlString() { - return """ - - - %s - - """.formatted(channelTag(this)); - } - - String channelTag(RSS2 rss) { - String channelItems = rss2ChannelItemsString(rss.getItems()); - Properties properties = new Properties(); - properties.put("title", rss.getTitle()); - properties.put("link", rss.getLink()); - properties.put("description", rss.getDescription()); - properties.put("lastBuildDate", Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME)); - properties.put("channelItems", channelItems); - return PLACEHOLDER_HELPER.replacePlaceholders(""" - - ${title} - ${link} - ${description} - ${lastBuildDate} - ${channelItems} - - """, properties); - } - - String rss2ChannelItemsString(List itemList) { - return itemList.stream() - .map(item -> { - Properties properties = new Properties(); - properties.put("title", item.getTitle()); - properties.put("link", item.getLink()); - properties.put("description", item.getDescription()); - properties.put("guid", item.guid); - properties.put("pubDate", item.pubDate.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME)); - return PLACEHOLDER_HELPER.replacePlaceholders(""" - - ${title} - ${link} - ${description} - ${guid} - ${pubDate} - - """, properties); - }) - .collect(Collectors.joining("\n")); - } -} \ No newline at end of file diff --git a/src/main/resources/extensions/ext-definition.yaml b/src/main/resources/extensions/ext-definition.yaml new file mode 100644 index 0000000..b65746c --- /dev/null +++ b/src/main/resources/extensions/ext-definition.yaml @@ -0,0 +1,19 @@ +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionDefinition +metadata: + name: moment-rss-provider +spec: + className: run.halo.moments.rss.MomentRssProvider + extensionPointName: feed-rss-route-item + displayName: "瞬间订阅" + description: "用于生成瞬间的 RSS 订阅源" +--- +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionDefinition +metadata: + name: moment-old-rss-redirection-filter +spec: + className: run.halo.moments.rss.OldRssRouteRedirectionFilter + extensionPointName: additional-webfilter + displayName: "瞬间旧 RSS 重定向" + description: "用于重定向旧的 RSS 订阅源到新的订阅路径" \ No newline at end of file diff --git a/src/main/resources/plugin.yaml b/src/main/resources/plugin.yaml index 2de3c3c..3810694 100644 --- a/src/main/resources/plugin.yaml +++ b/src/main/resources/plugin.yaml @@ -19,6 +19,8 @@ spec: issues: https://github.com/halo-sigs/plugin-moments/issues displayName: "瞬间" description: "Halo 2.0 的瞬间管理插件,提供一个轻量级的内容发布功能,支持发布图文、视频、音频等内容。" + pluginDependencies: + "PluginFeed?": ">=1.1.0" license: - name: "GPL-3.0" url: "https://github.com/halo-sigs/plugin-moments/blob/main/LICENSE"