1.4.3原版
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package la.moony.friends;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IModelFactory;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.dialect.TemplateHeadProcessor;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FriendsHeadProcessor implements TemplateHeadProcessor {
|
||||
|
||||
static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
|
||||
new PropertyPlaceholderHelper("${", "}");
|
||||
|
||||
private final PluginWrapper pluginWrapper;
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
final IModelFactory modelFactory = context.getModelFactory();
|
||||
model.add(modelFactory.createText(friendsRssComponentScript()));
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private String friendsRssComponentScript() {
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.setProperty("version", pluginWrapper.getDescriptor().getVersion());
|
||||
|
||||
return PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders("""
|
||||
<!-- plugin-friends-rss start -->
|
||||
<script defer src="/plugins/plugin-friends/assets/static/friends-rss.iife.js?version=${version}"></script>
|
||||
<link rel="stylesheet" href="/plugins/plugin-friends/assets/static/var.css?version=${version}" />
|
||||
<!-- plugin-friends-rss end -->
|
||||
""", properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package la.moony.friends;
|
||||
|
||||
import la.moony.friends.extension.CronFriendPost;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.Scheme;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexSpec;
|
||||
import run.halo.app.plugin.BasePlugin;
|
||||
import run.halo.app.plugin.PluginContext;
|
||||
|
||||
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
|
||||
|
||||
@Component
|
||||
public class FriendsPlugin extends BasePlugin {
|
||||
|
||||
private final SchemeManager schemeManager;
|
||||
|
||||
public FriendsPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
|
||||
super(pluginContext);
|
||||
this.schemeManager = schemeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
schemeManager.register(FriendPost.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.author")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getAuthor())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.title")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getTitle())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.description")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getDescription())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.postLink")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getPostLink())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.authorUrl")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getAuthorUrl())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.linkName")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(FriendPost.class, friendPost -> friendPost.getSpec().getLinkName())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.pubDate")
|
||||
.setIndexFunc(simpleAttribute(FriendPost.class, friendPost -> {
|
||||
var pubDate = friendPost.getSpec().getPubDate();
|
||||
return pubDate == null ? null : pubDate.toString();
|
||||
}))
|
||||
);
|
||||
});
|
||||
schemeManager.register(CronFriendPost.class);
|
||||
schemeManager.register(RssFeedSyncLog.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("state")
|
||||
.setIndexFunc(simpleAttribute(RssFeedSyncLog.class, rssFeedSyncLog -> {
|
||||
var state = rssFeedSyncLog.getState();
|
||||
return state == null ? null : state.name();
|
||||
})));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
schemeManager.unregister(Scheme.buildFromType(FriendPost.class));
|
||||
schemeManager.unregister(Scheme.buildFromType(CronFriendPost.class));
|
||||
schemeManager.unregister(Scheme.buildFromType(RssFeedSyncLog.class));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package la.moony.friends;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ListedRssSyncLog {
|
||||
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private Link link;
|
||||
|
||||
private RssFeedSyncLog log;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package la.moony.friends;
|
||||
|
||||
import la.moony.friends.extension.Link;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public class RssFeedSyncEvent extends ApplicationEvent {
|
||||
private final Link link;
|
||||
private final int sum;
|
||||
private final List<String> disableSyncList;
|
||||
|
||||
public RssFeedSyncEvent(Object source, Link link, int sum, List<String> disableSyncList) {
|
||||
super(source);
|
||||
this.link = link;
|
||||
this.sum = sum;
|
||||
this.disableSyncList = disableSyncList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package la.moony.friends.endpoint;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import la.moony.friends.ListedRssSyncLog;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import la.moony.friends.query.FriendPostQuery;
|
||||
import la.moony.friends.query.RssSyncLogQuery;
|
||||
import la.moony.friends.service.RssDetailService;
|
||||
import la.moony.friends.service.RssFeedSyncLogService;
|
||||
import la.moony.friends.vo.RssDetail;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
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.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
public class FriendEndpoint implements CustomEndpoint {
|
||||
|
||||
private final String tag = "api.friend.moony.la/v1alpha1/FriendPost";
|
||||
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final RssDetailService rssDetailService;
|
||||
|
||||
private final RssFeedSyncLogService rssFeedSyncLogService;
|
||||
|
||||
|
||||
public FriendEndpoint(ReactiveExtensionClient client, RssDetailService rssDetailService,
|
||||
RssFeedSyncLogService rssFeedSyncLogService) {
|
||||
this.client = client;
|
||||
this.rssDetailService = rssDetailService;
|
||||
this.rssFeedSyncLogService = rssFeedSyncLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
return SpringdocRouteBuilder.route()
|
||||
.GET("friendposts", this::listFriendPost, builder -> {
|
||||
builder.operationId("ListFriendPosts")
|
||||
.description("List friendPost.")
|
||||
.tag(tag)
|
||||
.response(
|
||||
responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(FriendPost.class))
|
||||
);
|
||||
FriendPostQuery.buildParameters(builder);
|
||||
})
|
||||
.DELETE("friendposts/-/delete", this::deleteAllRssFeedSyncLog, builder -> {
|
||||
builder.operationId("DeleteAllRssFeedSyncLog")
|
||||
.description("Delete All RssFeedSyncLog.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder()
|
||||
.implementation(Void.class));
|
||||
})
|
||||
.GET("rsssynclogs", this::listRssSyncLog, builder -> {
|
||||
builder.operationId("ListRssSyncLogs")
|
||||
.description("List RssSyncLog.")
|
||||
.tag(tag)
|
||||
.response(
|
||||
responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(ListedRssSyncLog.class))
|
||||
);
|
||||
RssSyncLogQuery.buildParameters(builder);
|
||||
})
|
||||
.GET("parsingrss", this::parsingRss, builder -> {
|
||||
builder.operationId("ParsingRss")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder()
|
||||
.name("rssUrl")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("fetchLimitNumber")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(false)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.response(
|
||||
responseBuilder()
|
||||
.implementation(RssDetail.class)
|
||||
);
|
||||
})
|
||||
.POST("syncrssfeed/{name}", this::syncRssFeed,
|
||||
builder -> builder.operationId("SyncRssFeed")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(Void.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
Mono<ServerResponse> listFriendPost(ServerRequest request) {
|
||||
FriendPostQuery friendPostQuery = new FriendPostQuery(request);
|
||||
return client.listBy(FriendPost.class, friendPostQuery.toListOptions(),friendPostQuery.toPageRequest())
|
||||
.flatMap(friendPosts -> ServerResponse.ok().bodyValue(friendPosts));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> deleteAllRssFeedSyncLog(ServerRequest request) {
|
||||
return deleteAllRssFeedSyncLog()
|
||||
.then(deleteAllFriendPost())
|
||||
.then(ServerResponse.ok().build());
|
||||
}
|
||||
|
||||
private Mono<Void> deleteAllRssFeedSyncLog() {
|
||||
ListOptions listOptions = new ListOptions().setFieldSelector(
|
||||
FieldSelector.of(isNull("metadata.deletionTimestamp")));
|
||||
|
||||
return client.listAll(RssFeedSyncLog.class, listOptions, Sort.unsorted())
|
||||
.flatMap(rssFeedSyncLog -> client.delete(rssFeedSyncLog))
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Void> deleteAllFriendPost() {
|
||||
ListOptions listOptions = new ListOptions().setFieldSelector(
|
||||
FieldSelector.of(isNull("metadata.deletionTimestamp")));
|
||||
|
||||
return client.listAll(FriendPost.class, listOptions, Sort.unsorted())
|
||||
.flatMap(friendPost -> client.delete(friendPost))
|
||||
.then();
|
||||
}
|
||||
|
||||
Mono<ServerResponse> listRssSyncLog(ServerRequest request) {
|
||||
RssSyncLogQuery rssSyncLogQuery = new RssSyncLogQuery(request);
|
||||
return rssFeedSyncLogService.listRssSyncLog(rssSyncLogQuery)
|
||||
.flatMap(listRssSyncLogs -> ServerResponse.ok().bodyValue(listRssSyncLogs));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> parsingRss(ServerRequest request) {
|
||||
final var rssUrl = request.queryParam("rssUrl").orElseThrow();
|
||||
final var fetchLimitNumber = request.queryParam("fetchLimitNumber")
|
||||
.map(Integer::parseInt)
|
||||
.orElse(5);
|
||||
return rssDetailService.fetchRssDetail(rssUrl,fetchLimitNumber)
|
||||
.flatMap(rssDetail -> ServerResponse.ok().bodyValue(rssDetail));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> syncRssFeed(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
|
||||
return rssFeedSyncLogService.publishRssFeedSyncEvent(name)
|
||||
.flatMap(rssFeedSyncEvent -> ServerResponse.ok().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("api.friend.moony.la/v1alpha1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package la.moony.friends.extension;
|
||||
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(
|
||||
group = "friend.moony.la",
|
||||
version = "v1alpha1",
|
||||
kind = "CronFriendPost",
|
||||
singular = "cronfriendpost",
|
||||
plural = "cronfriendposts"
|
||||
)
|
||||
public class CronFriendPost extends AbstractExtension {
|
||||
|
||||
|
||||
private CronSpec spec = new CronSpec();
|
||||
|
||||
private CronStatus status = new CronStatus();
|
||||
|
||||
@Data
|
||||
public static class CronSpec {
|
||||
private String cron;
|
||||
private String timezone;
|
||||
private boolean suspend;
|
||||
|
||||
@Schema(
|
||||
minimum = "0"
|
||||
)
|
||||
private int successfulRetainLimit;
|
||||
|
||||
private List<String> disableSyncList;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CronStatus {
|
||||
private Instant lastScheduledTimestamp;
|
||||
private Instant nextSchedulingTimestamp;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package la.moony.friends.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(kind = "FriendPost", group = "friend.moony.la",
|
||||
version = "v1alpha1", singular = "friendpost", plural = "friendposts")
|
||||
public class FriendPost extends AbstractExtension {
|
||||
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private FriendPostSpec spec;
|
||||
|
||||
@Data
|
||||
public static class FriendPostSpec {
|
||||
|
||||
private String author;
|
||||
|
||||
private String authorUrl;
|
||||
|
||||
private String logo;
|
||||
|
||||
private String title;
|
||||
|
||||
private String postLink;
|
||||
|
||||
private String description;
|
||||
|
||||
private Instant pubDate;
|
||||
|
||||
private String linkName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package la.moony.friends.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "core.halo.run", version = "v1alpha1",
|
||||
kind = "Link", plural = "links", singular = "link")
|
||||
public class Link extends AbstractExtension {
|
||||
|
||||
private LinkSpec spec;
|
||||
|
||||
@Data
|
||||
public static class LinkSpec {
|
||||
@Schema(required = true)
|
||||
private String url;
|
||||
|
||||
@Schema(required = true)
|
||||
private String displayName;
|
||||
|
||||
private String logo;
|
||||
|
||||
private String description;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
private String groupName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package la.moony.friends.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.LinkedHashSet;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "core.halo.run", version = "v1alpha1", kind = "LinkGroup", plural = "linkgroups", singular = "linkgroup")
|
||||
public class LinkGroup extends AbstractExtension {
|
||||
|
||||
private LinkGroupSpec spec;
|
||||
|
||||
@Data
|
||||
public static class LinkGroupSpec {
|
||||
@Schema(required = true)
|
||||
private String displayName;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
@Deprecated(since = "1.2.0", forRemoval = true)
|
||||
@Schema(description = "Names of links below this group.")
|
||||
@ArraySchema(arraySchema = @Schema(description = "Links of this group."), schema = @Schema(description = "Name of link."))
|
||||
private LinkedHashSet<String> links;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package la.moony.friends.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import java.time.Instant;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(kind = "RssFeedSyncLog", group = "friend.moony.la",
|
||||
version = "v1alpha1", singular = "rssfeedsynclog", plural = "rssfeedsynclogs")
|
||||
public class RssFeedSyncLog extends AbstractExtension {
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String linkName;
|
||||
|
||||
@Schema(requiredMode = REQUIRED,defaultValue = "nolink")
|
||||
private RssFeedSyncLogState state;
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Instant syncTime;
|
||||
|
||||
private String failureReason;
|
||||
|
||||
private String failureMessage;
|
||||
|
||||
public enum RssFeedSyncLogState {
|
||||
success,
|
||||
failed,
|
||||
onwait,
|
||||
nolink;
|
||||
|
||||
private RssFeedSyncLogState() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package la.moony.friends.finders;
|
||||
|
||||
import la.moony.friends.finders.impl.FriendFinderImpl;
|
||||
import la.moony.friends.vo.FriendPostVo;
|
||||
import la.moony.friends.vo.LinkGroupVo;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import java.util.Map;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
public interface FriendFinder {
|
||||
|
||||
Flux<FriendPostVo> listAll();
|
||||
|
||||
Mono<ListResult<FriendPostVo>> list(@Nullable Integer page, @Nullable Integer size);
|
||||
|
||||
/**
|
||||
* Lists friendPosts by query params.
|
||||
*
|
||||
* @param params query params see {@link FriendFinderImpl.FriendPostQuery}
|
||||
*/
|
||||
Mono<ListResult<FriendPostVo>> list(Map<String, Object> params);
|
||||
|
||||
Mono<FriendPostVo> getByName(String friendPostName);
|
||||
|
||||
Flux<LinkGroupVo> linkGroupBy();
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package la.moony.friends.finders.impl;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.LinkGroup;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import la.moony.friends.finders.FriendFinder;
|
||||
import la.moony.friends.util.SortUtils;
|
||||
import la.moony.friends.vo.FriendPostVo;
|
||||
import la.moony.friends.vo.LinkGroupVo;
|
||||
import la.moony.friends.vo.LinkVo;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.extension.*;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.springframework.data.domain.Sort.Order.asc;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.*;
|
||||
|
||||
|
||||
@Finder("friendFinder")
|
||||
@RequiredArgsConstructor
|
||||
public class FriendFinderImpl implements FriendFinder {
|
||||
|
||||
static final String UNGROUPED_NAME = "ungrouped";
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<FriendPostVo> listAll() {
|
||||
var listOptions = new ListOptions();
|
||||
var query = all();
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
return client.listAll(FriendPost.class, listOptions, defaultSort())
|
||||
.flatMap(this::getFriendPostVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<FriendPostVo>> list(Integer page, Integer size) {
|
||||
return pageFriendPost(new ListOptions(), getPageRequest(page, size));
|
||||
}
|
||||
|
||||
private PageRequestImpl getPageRequest(Integer page, Integer size) {
|
||||
return PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<FriendPostVo>> list(Map<String, Object> params) {
|
||||
var query = Optional.ofNullable(params)
|
||||
.map(map -> JsonUtils.mapToObject(map, FriendPostQuery.class))
|
||||
.orElseGet(FriendPostQuery::new);
|
||||
return pageFriendPost(query.toListOptions(), query.toPageRequest());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<FriendPostVo> getByName(String friendPostName) {
|
||||
return client.fetch(FriendPost.class, friendPostName)
|
||||
.map(FriendPostVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<LinkGroupVo> linkGroupBy() {
|
||||
return client.listAll(LinkGroup.class, new ListOptions(), defaultGroupSort())
|
||||
.map(LinkGroupVo::from)
|
||||
.concatMap(group -> linkListBy(group.getMetadata().getName())
|
||||
.collectList()
|
||||
.map(group::withLinks)
|
||||
.defaultIfEmpty(group)
|
||||
)
|
||||
.mergeWith(Mono.defer(() -> linkListBy(UNGROUPED_NAME)
|
||||
.collectList()
|
||||
// do not return ungrouped group if no links
|
||||
.filter(links -> !links.isEmpty())
|
||||
.flatMap(links -> ungrouped()
|
||||
.map(LinkGroupVo::from)
|
||||
.map(group -> group.withLinks(links))
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
private Mono<ListResult<FriendPostVo>> pageFriendPost(ListOptions queryOptions, PageRequest page){
|
||||
var listOptions = new ListOptions();
|
||||
var query = all();
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
var fieldSelector = queryOptions.getFieldSelector();
|
||||
if (fieldSelector != null) {
|
||||
listOptions.setFieldSelector(listOptions.getFieldSelector()
|
||||
.andQuery(fieldSelector.query()));
|
||||
}
|
||||
|
||||
return client.listBy(FriendPost.class, listOptions, page)
|
||||
.flatMap(list -> Flux.fromStream(list.get())
|
||||
.concatMap(this::getFriendPostVo)
|
||||
.collectList()
|
||||
.map(friendPostVos -> new ListResult<>(list.getPage(), list.getSize(),
|
||||
list.getTotal(), friendPostVos)
|
||||
)
|
||||
)
|
||||
.defaultIfEmpty(ListResult.emptyResult());
|
||||
|
||||
}
|
||||
|
||||
public Flux<LinkVo> linkListBy(String groupName) {
|
||||
var listOptions = new ListOptions();
|
||||
var query = isNull("metadata.deletionTimestamp");
|
||||
if (UNGROUPED_NAME.equals(groupName)) {
|
||||
query = and(query, isNull("spec.groupName"));
|
||||
} else {
|
||||
query = and(query, equal("spec.groupName", groupName));
|
||||
}
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
return client.listAll(Link.class, listOptions, defaultLinkSort())
|
||||
.flatMapSequential(this::convertToListedVo)
|
||||
.collectList()
|
||||
.map(links -> {
|
||||
// 排序逻辑
|
||||
return links.stream()
|
||||
.sorted(Comparator.comparing(link -> {
|
||||
List<FriendPostVo> friendPosts = link.getFriendPosts();
|
||||
if (friendPosts != null && !friendPosts.isEmpty()) {
|
||||
return friendPosts.get(0).getSpec().getPubDate();
|
||||
}
|
||||
return null;
|
||||
}, Comparator.nullsLast(Comparator.reverseOrder())
|
||||
)).collect(Collectors.toList());
|
||||
})
|
||||
.flatMapMany(Flux::fromIterable);
|
||||
}
|
||||
|
||||
|
||||
public Mono<LinkVo> convertToListedVo(@NonNull Link link) {
|
||||
Assert.notNull(link, "Link must not be null");
|
||||
LinkVo linkVo = LinkVo.from(link);
|
||||
linkVo.setFriendPosts(List.of());
|
||||
return Mono.just(linkVo)
|
||||
.flatMap(lp -> friendPostListBy(lp.getMetadata().getName())
|
||||
.doOnNext(lp::setFriendPosts)
|
||||
.thenReturn(lp)
|
||||
).flatMap(p -> {
|
||||
String linkName = p.getMetadata().getName();
|
||||
return client.fetch(RssFeedSyncLog.class,"sync-log-" + linkName)
|
||||
.doOnNext(p::setRssFeedSyncLog)
|
||||
.thenReturn(p);
|
||||
})
|
||||
.defaultIfEmpty(linkVo);
|
||||
}
|
||||
|
||||
public Mono<List<FriendPostVo>> friendPostListBy(String linkName) {
|
||||
var listOptions = new ListOptions();
|
||||
var query = isNull("metadata.deletionTimestamp");
|
||||
query = and(query, equal("spec.linkName", linkName));
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
var pageRequest = PageRequestImpl.of(pageNullSafe(1), sizeNullSafe(2), defaultSort());
|
||||
return client.listBy(FriendPost.class, listOptions, pageRequest)
|
||||
.flatMap(friendPosts -> Flux.fromStream(friendPosts.get())
|
||||
.map(FriendPostVo::from).collectList());
|
||||
}
|
||||
|
||||
Mono<LinkGroup> ungrouped() {
|
||||
LinkGroup linkGroup = new LinkGroup();
|
||||
linkGroup.setMetadata(new Metadata());
|
||||
linkGroup.getMetadata().setName(UNGROUPED_NAME);
|
||||
linkGroup.setSpec(new LinkGroup.LinkGroupSpec());
|
||||
linkGroup.getSpec().setDisplayName("");
|
||||
linkGroup.getSpec().setPriority(0);
|
||||
return Mono.just(linkGroup);
|
||||
}
|
||||
|
||||
static Sort defaultGroupSort() {
|
||||
return Sort.by(asc("spec.priority"),
|
||||
asc("metadata.creationTimestamp"),
|
||||
asc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
static Sort defaultLinkSort() {
|
||||
return Sort.by(asc("spec.priority"),
|
||||
asc("metadata.creationTimestamp"),
|
||||
asc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
static Sort defaultSort() {
|
||||
return Sort.by("spec.pubDate").descending();
|
||||
}
|
||||
|
||||
private Mono<FriendPostVo> getFriendPostVo(@Nonnull FriendPost friendPost) {
|
||||
FriendPostVo friendPostVo = FriendPostVo.from(friendPost);
|
||||
return Mono.just(friendPostVo);
|
||||
}
|
||||
|
||||
static int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
||||
static int sizeNullSafe(Integer size) {
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FriendPostQuery {
|
||||
private Integer page;
|
||||
private Integer size;
|
||||
private String linkName;
|
||||
private List<String> sort;
|
||||
|
||||
public ListOptions toListOptions() {
|
||||
var builder = ListOptions.builder();
|
||||
if (StringUtils.isNotBlank(linkName)) {
|
||||
builder.andQuery(equal("spec.linkName", linkName));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public PageRequest toPageRequest() {
|
||||
return PageRequestImpl.of(pageNullSafe(getPage()),
|
||||
sizeNullSafe(getSize()), SortUtils.resolve(sort).and(defaultSort()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package la.moony.friends.query;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.operation.Builder;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.SortableRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springframework.data.domain.Sort.Order.desc;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.*;
|
||||
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
|
||||
|
||||
public class FriendPostQuery extends SortableRequest {
|
||||
|
||||
public FriendPostQuery(ServerRequest request) {
|
||||
super(request.exchange());
|
||||
}
|
||||
@Nullable
|
||||
public String getKeyword() {
|
||||
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getLinkName() {
|
||||
return queryParams.getFirst("linkName");
|
||||
}
|
||||
|
||||
|
||||
public ListOptions toListOptions() {
|
||||
var builder = ListOptions.builder(super.toListOptions());
|
||||
|
||||
Optional.ofNullable(getKeyword())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.ifPresent(keyword -> builder.andQuery(or(
|
||||
contains("spec.author", keyword),
|
||||
contains("spec.authorUrl", keyword),
|
||||
contains("spec.title", keyword)
|
||||
)));
|
||||
|
||||
Optional.ofNullable(getLinkName())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.ifPresent(linkName -> builder.andQuery(equal("spec.linkName", linkName)));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public Comparator<FriendPost> toComparator() {
|
||||
List<Comparator<FriendPost>> comparators = new ArrayList<>();
|
||||
var sort = getSort();
|
||||
var ctOrder = sort.getOrderFor("pubDate");
|
||||
if (ctOrder != null) {
|
||||
Comparator<FriendPost> comparator =
|
||||
comparing(friendPost -> friendPost.getSpec().getPubDate());
|
||||
if (ctOrder.isDescending()) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
comparators.add(comparator);
|
||||
}
|
||||
Comparator<FriendPost> comparator =
|
||||
comparing(friendPost -> friendPost.getSpec().getPubDate());
|
||||
comparators.add(comparator.reversed());
|
||||
return comparators.stream()
|
||||
.reduce(Comparator::thenComparing)
|
||||
.orElse(null);
|
||||
|
||||
}
|
||||
|
||||
public PageRequest toPageRequest() {
|
||||
var sort = SortResolver.defaultInstance.resolve(exchange).and(defaultSort());
|
||||
return PageRequestImpl.of(getPage(), getSize(), sort);
|
||||
}
|
||||
|
||||
public static Sort defaultSort() {
|
||||
return Sort.by(desc("spec.pubDate"));
|
||||
}
|
||||
|
||||
public static void buildParameters(Builder builder) {
|
||||
IListRequest.buildParameters(builder);
|
||||
builder.parameter(sortParameter())
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("linkName")
|
||||
.description("CronFriendPost filtered by linkName.")
|
||||
.implementation(String.class)
|
||||
.required(false))
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("keyword")
|
||||
.description("CronFriendPost filtered by keyword.")
|
||||
.implementation(String.class)
|
||||
.required(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package la.moony.friends.query;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.operation.Builder;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.SortableRequest;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.*;
|
||||
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
|
||||
|
||||
public class RssSyncLogQuery extends SortableRequest {
|
||||
|
||||
public RssSyncLogQuery(ServerRequest request) {
|
||||
super(request.exchange());
|
||||
}
|
||||
@Nullable
|
||||
public String getKeyword() {
|
||||
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getState() {
|
||||
return queryParams.getFirst("state");
|
||||
}
|
||||
|
||||
|
||||
public ListOptions toListOptions() {
|
||||
var builder = ListOptions.builder(super.toListOptions());
|
||||
|
||||
Optional.ofNullable(getKeyword())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.ifPresent(keyword -> builder.andQuery(or(
|
||||
contains("metadata.name", keyword),
|
||||
contains("spec.displayName", keyword)
|
||||
)));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static void buildParameters(Builder builder) {
|
||||
IListRequest.buildParameters(builder);
|
||||
builder.parameter(sortParameter())
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("keyword")
|
||||
.implementation(String.class)
|
||||
.required(false))
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("state")
|
||||
.implementation(String.class)
|
||||
.required(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package la.moony.friends.reconciler;
|
||||
|
||||
import la.moony.friends.extension.CronFriendPost;
|
||||
import la.moony.friends.service.RssFeedSyncLogService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import java.time.*;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class CronCronFriendPostReconciler implements Reconciler<Reconciler.Request>{
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CronCronFriendPostReconciler.class);
|
||||
|
||||
private final ExtensionClient client;
|
||||
private Clock clock;
|
||||
private final RssFeedSyncLogService rssFeedSyncLogService;
|
||||
|
||||
|
||||
public CronCronFriendPostReconciler(ExtensionClient client,RssFeedSyncLogService rssFeedSyncLogService) {
|
||||
this.client = client;
|
||||
this.rssFeedSyncLogService = rssFeedSyncLogService;
|
||||
this.clock = Clock.systemDefaultZone();
|
||||
}
|
||||
|
||||
|
||||
void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
|
||||
public Result reconcile(Request request) {
|
||||
return (Result)this.client.fetch(CronFriendPost.class, request.name()).map((cronFriendPost) -> {
|
||||
if (ExtensionUtil.isDeleted(cronFriendPost)) {
|
||||
return Result.doNotRetry();
|
||||
} else {
|
||||
CronFriendPost.CronSpec spec = cronFriendPost.getSpec();
|
||||
if (!spec.isSuspend()) {
|
||||
return Result.doNotRetry();
|
||||
} else {
|
||||
String cron = spec.getCron();
|
||||
String timezone = spec.getTimezone();
|
||||
ZoneId zoneId = ZoneId.systemDefault();
|
||||
if (timezone != null) {
|
||||
try {
|
||||
zoneId = ApplicationConversionService.getSharedInstance().convert(timezone, ZoneId.class);
|
||||
} catch (DateTimeException dateTimeException) {
|
||||
log.error("Invalid zone ID {}", timezone, dateTimeException);
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
}
|
||||
|
||||
Instant now = Instant.now(this.clock);
|
||||
if (!CronExpression.isValidExpression(cron)) {
|
||||
log.error("Cron expression {} is invalid.", cron);
|
||||
return Result.doNotRetry();
|
||||
} else {
|
||||
CronExpression cronExp = CronExpression.parse(cron);
|
||||
CronFriendPost.CronStatus status = cronFriendPost.getStatus();
|
||||
Instant lastScheduledTimestamp = status.getLastScheduledTimestamp();
|
||||
if (lastScheduledTimestamp == null) {
|
||||
lastScheduledTimestamp = cronFriendPost.getMetadata().getCreationTimestamp();
|
||||
}
|
||||
|
||||
ZonedDateTime nextFromNow = cronExp.next(now.atZone(zoneId));
|
||||
ZonedDateTime nextFromLast = cronExp.next(lastScheduledTimestamp.atZone(zoneId));
|
||||
|
||||
if (nextFromNow != null && nextFromLast != null) {
|
||||
if (Objects.equals(nextFromNow, nextFromLast)) {
|
||||
log.info("Skip scheduling and next scheduled at {}", nextFromNow);
|
||||
status.setNextSchedulingTimestamp(nextFromNow.toInstant());
|
||||
this.client.update(cronFriendPost);
|
||||
return new Result(true, Duration.between(now, nextFromNow));
|
||||
} else {
|
||||
|
||||
|
||||
rssFeedSyncLogService.publishRssFeedSyncEvent("all").block();
|
||||
|
||||
ZonedDateTime zonedNow = now.atZone(zoneId);
|
||||
ZonedDateTime scheduleTimestamp = now.atZone(zoneId);
|
||||
|
||||
ZonedDateTime next;
|
||||
for(next = lastScheduledTimestamp.atZone(zoneId); next != null && next.isBefore(zonedNow); next = cronExp.next(next)) {
|
||||
scheduleTimestamp = next;
|
||||
}
|
||||
|
||||
status.setLastScheduledTimestamp(scheduleTimestamp.toInstant());
|
||||
if (next != null) {
|
||||
status.setNextSchedulingTimestamp(next.toInstant());
|
||||
}
|
||||
|
||||
this.client.update(cronFriendPost);
|
||||
log.info("Scheduled at {} and next scheduled at {}", scheduleTimestamp, next);
|
||||
return new Result(true, Duration.between(now, next));
|
||||
}
|
||||
|
||||
} else {
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}).orElseGet(Result::doNotRetry);
|
||||
}
|
||||
|
||||
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder.extension(new CronFriendPost()).workerCount(1).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package la.moony.friends.reconciler;
|
||||
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.Link;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LinkReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(Link.class, request.name())
|
||||
.ifPresent(link -> {
|
||||
if (ExtensionUtil.isDeleted(link)) {
|
||||
var friendPostListOptions = new ListOptions();
|
||||
friendPostListOptions.setFieldSelector(FieldSelector.of(
|
||||
equal("spec.linkName",link.getMetadata().getName())
|
||||
));
|
||||
client.listAll(FriendPost.class,friendPostListOptions, Sort.by("metadata.creationTimestamp").descending())
|
||||
.forEach(friendPost -> client.delete(friendPost));
|
||||
}
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new Link())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package la.moony.friends.reconciler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rometools.rome.feed.synd.SyndContent;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
import com.rometools.rome.io.XmlReader;
|
||||
import la.moony.friends.RssFeedSyncEvent;
|
||||
import la.moony.friends.extension.CronFriendPost;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import la.moony.friends.util.CommonUtils;
|
||||
import la.moony.friends.util.LinkRequest;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.extension.controller.*;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.data.domain.Sort.Order.asc;
|
||||
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,SmartLifecycle {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RssSyncReconciler.class);
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final RequestQueue<Request> queue;
|
||||
private final Controller controller;
|
||||
private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;
|
||||
|
||||
|
||||
public RssSyncReconciler(ExtensionClient client) {
|
||||
this.client = client;
|
||||
queue = new DefaultQueue<>(Instant::now);
|
||||
controller = this.setupWith(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
Link link = request.link();
|
||||
int sum = request.sum();
|
||||
List<String> disableSyncList = request.disableSyncList();
|
||||
var linkName = link.getMetadata().getName();
|
||||
String rssUrl = getRss(link);
|
||||
boolean isContains = false;
|
||||
if (ObjectUtils.isNotEmpty(disableSyncList)) {
|
||||
isContains = disableSyncList.contains(linkName);
|
||||
}
|
||||
String syncLogName = "sync-log-" + linkName;
|
||||
RssFeedSyncLog newRssFeedSyncLog = new RssFeedSyncLog();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(syncLogName);
|
||||
newRssFeedSyncLog.setMetadata(metadata);
|
||||
newRssFeedSyncLog.setLinkName(linkName);
|
||||
if (StringUtils.isNotEmpty(rssUrl) && !isContains) {
|
||||
tryToSynchronizeFriendPost(newRssFeedSyncLog, link, sum);
|
||||
}else if (StringUtils.isEmpty(rssUrl)) {
|
||||
newRssFeedSyncLog.setSyncTime(Instant.now());
|
||||
newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.nolink);
|
||||
newRssFeedSyncLog.setFailureReason("No RSS link");
|
||||
newRssFeedSyncLog.setFailureMessage("无 RSS 链接.");
|
||||
}else if (isContains) {
|
||||
newRssFeedSyncLog.setSyncTime(Instant.now());
|
||||
newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.failed);
|
||||
newRssFeedSyncLog.setFailureReason("admin disables sync");
|
||||
newRssFeedSyncLog.setFailureMessage("管理员禁用同步.");
|
||||
}
|
||||
Optional<RssFeedSyncLog> fetch = client.fetch(RssFeedSyncLog.class, syncLogName);
|
||||
if (fetch.isPresent()) {
|
||||
RssFeedSyncLog rssFeedSyncLog = fetch.get();
|
||||
rssFeedSyncLog.setState(newRssFeedSyncLog.getState());
|
||||
rssFeedSyncLog.setSyncTime(newRssFeedSyncLog.getSyncTime());
|
||||
rssFeedSyncLog.setFailureReason(newRssFeedSyncLog.getFailureReason());
|
||||
rssFeedSyncLog.setFailureMessage(newRssFeedSyncLog.getFailureMessage());
|
||||
client.update(rssFeedSyncLog);
|
||||
}else {
|
||||
client.create(newRssFeedSyncLog);
|
||||
}
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
public String getRss(Link link) {
|
||||
var annotations = nullSafeAnnotations(link);
|
||||
|
||||
//处理旧数据 rss_uri
|
||||
String rssUri = annotations.get("rss_uri");
|
||||
|
||||
String rssUrl = annotations.get("rss_url");
|
||||
String isRequest = annotations.get("is_request");
|
||||
if ((StringUtils.isEmpty(rssUrl) && StringUtils.isEmpty(isRequest)) || StringUtils.isNotEmpty(rssUri)) {
|
||||
String linkRss = LinkRequest.getLinkRss(link.getSpec().getUrl());
|
||||
if (StringUtils.isNotEmpty(rssUri)) {
|
||||
linkRss = rssUri;
|
||||
annotations.remove("rss_uri");
|
||||
annotations.put("rss_url", rssUri);
|
||||
}else {
|
||||
annotations.put("is_request","true");
|
||||
if (StringUtils.isNotEmpty(linkRss)) {
|
||||
annotations.put("rss_url",linkRss);
|
||||
}
|
||||
}
|
||||
updateLink(link);
|
||||
return linkRss;
|
||||
}
|
||||
return rssUrl;
|
||||
}
|
||||
|
||||
|
||||
public void updateLink(Link link) {
|
||||
Map extensionMap = objectMapper.convertValue(link, Map.class);
|
||||
var extension = new Unstructured(extensionMap);
|
||||
client.update(extension);
|
||||
}
|
||||
|
||||
private void tryToSynchronizeFriendPost(RssFeedSyncLog syncLog, Link link, int sum) {
|
||||
var linkName = link.getMetadata().getName();
|
||||
var annotations = nullSafeAnnotations(link);
|
||||
var rssUrl = annotations.get("rss_url");
|
||||
|
||||
CronFriendPost cron = new CronFriendPost();
|
||||
var spec = new CronFriendPost.CronSpec();
|
||||
spec.setSuccessfulRetainLimit(5);
|
||||
cron.setSpec(spec);
|
||||
syncLog.setSyncTime(Instant.now());
|
||||
List<FriendPost> friendPostList = fetchFriendPost(rssUrl, sum, syncLog);
|
||||
|
||||
// 即使获取失败也继续处理
|
||||
if (friendPostList != null && !friendPostList.isEmpty()) {
|
||||
for (FriendPost rssPost : friendPostList) {
|
||||
var friendPostListOptions = new ListOptions();
|
||||
friendPostListOptions.setFieldSelector(FieldSelector.of(
|
||||
and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.postLink",rssPost.getSpec().getPostLink())
|
||||
)
|
||||
));
|
||||
long total = client.listBy(FriendPost.class, friendPostListOptions,
|
||||
PageRequestImpl.ofSize(1)).getTotal();
|
||||
if (total==0) {
|
||||
FriendPost friendPost = new FriendPost();
|
||||
// 设置元数据才能保存
|
||||
friendPost.setMetadata(new Metadata());
|
||||
friendPost.getMetadata().setGenerateName("friend-post-");
|
||||
friendPost.setSpec(rssPost.getSpec());
|
||||
var friendPostSpec = friendPost.getSpec();
|
||||
friendPostSpec.setLogo(link.getSpec().getLogo());
|
||||
friendPostSpec.setAuthor(link.getSpec().getDisplayName());
|
||||
friendPostSpec.setAuthorUrl(link.getSpec().getUrl());
|
||||
friendPostSpec.setLinkName(linkName);
|
||||
String description = friendPost.getSpec().getDescription();
|
||||
//解析html内容转换成文本
|
||||
if (StringUtils.isNotEmpty(description)){
|
||||
description = CommonUtils.parseAndTruncateHtml2Text(description, 200);
|
||||
String regexp = "[ *|\\s*]*";
|
||||
description = description.replaceFirst(regexp, "").trim();
|
||||
friendPostSpec.setDescription(description);
|
||||
}
|
||||
client.create(friendPost);
|
||||
}
|
||||
}
|
||||
delFriendPost(linkName,sum);
|
||||
}
|
||||
}
|
||||
|
||||
public List<FriendPost> fetchFriendPost(String rssAddress,int postsLimit, RssFeedSyncLog sync) {
|
||||
List<FriendPost> friendPostList = new ArrayList<>();
|
||||
try {
|
||||
SyndFeed feed = new SyndFeedInput().build(new XmlReader(new URL(rssAddress)));
|
||||
int postCount = 0;
|
||||
for (SyndEntry entry : feed.getEntries()) {
|
||||
Date date = entry.getPublishedDate();
|
||||
if (date == null) {
|
||||
date = entry.getUpdatedDate();
|
||||
}
|
||||
String description = "";
|
||||
SyndContent content = entry.getDescription();
|
||||
if (content != null) {
|
||||
description = content.getValue();
|
||||
}else {
|
||||
List<SyndContent> contents = entry.getContents();
|
||||
if (!contents.isEmpty()) {
|
||||
description = contents.get(0).getValue();
|
||||
}
|
||||
}
|
||||
|
||||
FriendPost friendPost = new FriendPost();
|
||||
friendPost.setSpec(new FriendPost.FriendPostSpec());
|
||||
friendPost.getSpec().setTitle(entry.getTitle());
|
||||
friendPost.getSpec().setPostLink(entry.getLink());
|
||||
friendPost.getSpec().setPubDate(date.toInstant());
|
||||
friendPost.getSpec().setDescription(description);
|
||||
friendPostList.add(friendPost);
|
||||
postCount++;
|
||||
if (postCount >= postsLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sync.setState(RssFeedSyncLog.RssFeedSyncLogState.success);
|
||||
} catch (Exception e) {
|
||||
sync.setState(RssFeedSyncLog.RssFeedSyncLogState.failed);
|
||||
sync.setFailureReason("CrawlingRSSError");
|
||||
sync.setFailureMessage(e.getMessage());
|
||||
log.error("error in crawling rss", e);
|
||||
return null;
|
||||
}
|
||||
return friendPostList;
|
||||
}
|
||||
|
||||
public void delFriendPost(String linkName,Integer sum) {
|
||||
var friendPostListOptions = new ListOptions();
|
||||
friendPostListOptions.setFieldSelector(FieldSelector.of(
|
||||
equal("spec.linkName",linkName)
|
||||
));
|
||||
long total = client.listBy(FriendPost.class, friendPostListOptions, PageRequestImpl.ofSize(1)).getTotal();
|
||||
if (total > sum) {
|
||||
int l = (int) (total - sum);
|
||||
PageRequestImpl pageRequestImpl = PageRequestImpl.of(pageNullSafe(1), sizeNullSafe(l),
|
||||
Sort.by(asc("spec.pubDate")));
|
||||
ListResult<FriendPost> friendPosts =
|
||||
client.listBy(FriendPost.class, friendPostListOptions, pageRequestImpl);
|
||||
for (FriendPost friendPost : friendPosts) {
|
||||
client.delete(friendPost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
||||
static int sizeNullSafe(Integer size) {
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
this.getClass().getName(),
|
||||
this,
|
||||
queue,
|
||||
null,
|
||||
Duration.ofMillis(300),
|
||||
Duration.ofMinutes(5),
|
||||
5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.controller.start();
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.running = false;
|
||||
this.controller.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@EventListener(RssFeedSyncEvent.class)
|
||||
public void onReplyEvent(RssFeedSyncEvent event) {
|
||||
var request = new Request(event.getLink(),event.getSum(),event.getDisableSyncList());
|
||||
queue.addImmediately(request);
|
||||
}
|
||||
|
||||
public record Request(Link link,int sum,List<String> disableSyncList) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package la.moony.friends.rest;
|
||||
|
||||
import la.moony.friends.finders.FriendFinder;
|
||||
import la.moony.friends.vo.FriendPostVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.ReactiveSettingFetcher;
|
||||
import run.halo.app.theme.TemplateNameResolver;
|
||||
import run.halo.app.theme.router.ModelConst;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FriendRouter {
|
||||
|
||||
|
||||
private final FriendFinder friendFinder;
|
||||
|
||||
private final TemplateNameResolver templateNameResolver;
|
||||
|
||||
private final ReactiveSettingFetcher settingFetcher;
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> friendTemplateRoute() {
|
||||
return RouterFunctions.route().GET("/friends",this::handlerFunction)
|
||||
.GET("/friends/page/{page:\\d+}",this::handlerFunction)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private Mono<ServerResponse> handlerFunction(ServerRequest request) {
|
||||
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friends")
|
||||
.flatMap( templateName -> ServerResponse.ok().render(templateName,
|
||||
java.util.Map.of(ModelConst.TEMPLATE_ID, templateName,
|
||||
"friends", friendPostList(request),
|
||||
"title",getFriendTitle())
|
||||
));
|
||||
}
|
||||
|
||||
Mono<String> getFriendTitle() {
|
||||
return this.settingFetcher.get("base").map(
|
||||
setting -> setting.get("title").asText("友链朋友圈")).defaultIfEmpty(
|
||||
"友链朋友圈");
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<FriendPostVo>> friendPostList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
String linkName = request.queryParam("linkName")
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElse(null);
|
||||
return this.settingFetcher.get("base")
|
||||
.map(item -> item.get("pageSize").asInt(10))
|
||||
.defaultIfEmpty(10)
|
||||
.flatMap(pageSize -> {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("page",pageNum);
|
||||
params.put("size",pageSize);
|
||||
if(StringUtils.isNotBlank(linkName)){
|
||||
params.put("linkName",linkName);
|
||||
}
|
||||
return friendFinder.list(params)
|
||||
.map(list -> new UrlContextListResult.Builder<FriendPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private int pageNumInPathVariable(ServerRequest request) {
|
||||
String page = request.pathVariables().get("page");
|
||||
return NumberUtils.toInt(page, 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package la.moony.friends.service;
|
||||
|
||||
import la.moony.friends.vo.RssDetail;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface RssDetailService {
|
||||
|
||||
Mono<RssDetail> fetchRssDetail(String rssUrl, int fetchLimitNumber);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package la.moony.friends.service;
|
||||
|
||||
import la.moony.friends.ListedRssSyncLog;
|
||||
import la.moony.friends.query.RssSyncLogQuery;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
||||
public interface RssFeedSyncLogService {
|
||||
|
||||
Mono<ListResult<ListedRssSyncLog>> listRssSyncLog(RssSyncLogQuery query);
|
||||
|
||||
Mono<Void> publishRssFeedSyncEvent(String name);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package la.moony.friends.service.impl;
|
||||
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
import com.rometools.rome.io.XmlReader;
|
||||
import la.moony.friends.service.RssDetailService;
|
||||
import la.moony.friends.vo.RssDetail;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class RssDetailServiceImpl implements RssDetailService {
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<RssDetail> fetchRssDetail(String rssUrl, int fetchLimitNumber) {
|
||||
RssDetail rssDetail = new RssDetail();
|
||||
try {
|
||||
// 创建 SyndFeedInput 对象并从 URL 获取 RSS 提要
|
||||
SyndFeedInput input = new SyndFeedInput();
|
||||
SyndFeed feed = input.build(new XmlReader(new URL(rssUrl)));
|
||||
RssDetail.Channel channel = new RssDetail.Channel();
|
||||
channel.setTitle(feed.getTitle());
|
||||
channel.setLink(feed.getLink());
|
||||
channel.setDescription(feed.getDescription());
|
||||
rssDetail.setChannel(channel);
|
||||
List<RssDetail.Item> items = new ArrayList<>();
|
||||
int count = 0;
|
||||
for (SyndEntry syndEntry : feed.getEntries()) {
|
||||
RssDetail.Item item = new RssDetail.Item();
|
||||
item.setTitle(syndEntry.getTitle());
|
||||
item.setLink(syndEntry.getLink());
|
||||
item.setAuthor(syndEntry.getAuthor());
|
||||
item.setDescription(syndEntry.getDescription().getValue());
|
||||
item.setPubDate(syndEntry.getPublishedDate().toInstant());
|
||||
items.add(item);
|
||||
count++;
|
||||
if (count >= fetchLimitNumber) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
channel.setItems(items);
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return Mono.just(rssDetail);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package la.moony.friends.service.impl;
|
||||
|
||||
import la.moony.friends.ListedRssSyncLog;
|
||||
import la.moony.friends.RssFeedSyncEvent;
|
||||
import la.moony.friends.extension.CronFriendPost;
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import la.moony.friends.query.RssSyncLogQuery;
|
||||
import la.moony.friends.service.RssFeedSyncLogService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
public class RssFeedSyncLogServiceImpl implements RssFeedSyncLogService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public RssFeedSyncLogServiceImpl(ReactiveExtensionClient client,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
this.client = client;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedRssSyncLog>> listRssSyncLog(RssSyncLogQuery query) {
|
||||
return buildListOptions(query)
|
||||
.flatMap(listOptions -> client.listBy(Link.class, listOptions,
|
||||
PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
|
||||
))
|
||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||
.map(this::getListedRssSyncLog)
|
||||
.flatMapSequential(Function.identity())
|
||||
.collectList()
|
||||
.map(listedApplications -> new ListResult<>(listResult.getPage(), listResult.getSize(),
|
||||
listResult.getTotal(), listedApplications)
|
||||
)
|
||||
.defaultIfEmpty(ListResult.emptyResult())
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ListedRssSyncLog> getListedRssSyncLog(Link link) {
|
||||
Assert.notNull(link, "The link must not be null.");
|
||||
var listedRssSyncLog = new ListedRssSyncLog().setLink(link);
|
||||
var logMono = client.fetch(RssFeedSyncLog.class,"sync-log-"+link.getMetadata().getName())
|
||||
.doOnNext(listedRssSyncLog::setLog);
|
||||
|
||||
return Mono.when(logMono)
|
||||
.thenReturn(listedRssSyncLog);
|
||||
}
|
||||
|
||||
Mono<ListOptions> buildListOptions(RssSyncLogQuery query) {
|
||||
return Mono.just(query.toListOptions());
|
||||
}
|
||||
|
||||
public Mono<Void> publishRssFeedSyncEvent(String name) {
|
||||
CronFriendPost cron = new CronFriendPost();
|
||||
CronFriendPost.CronSpec cronSpec = new CronFriendPost.CronSpec();
|
||||
cronSpec.setSuccessfulRetainLimit(5);
|
||||
cronSpec.setDisableSyncList(new ArrayList<>());
|
||||
cron.setSpec(cronSpec);
|
||||
return client.fetch(CronFriendPost.class, "cron-friends-default")
|
||||
.defaultIfEmpty(cron)
|
||||
.flatMap(cronFriendPost -> {
|
||||
var spec = cronFriendPost.getSpec();
|
||||
int successfulRetainLimit = spec.getSuccessfulRetainLimit();
|
||||
List<String> disableSyncList = spec.getDisableSyncList();
|
||||
int sum = successfulRetainLimit == 0 ? 5 : successfulRetainLimit;
|
||||
var listOptions = new ListOptions();
|
||||
FieldSelector fieldSelector = FieldSelector.of(isNull("metadata.deletionTimestamp"));
|
||||
if (!StringUtils.equals(name,"all")) {
|
||||
fieldSelector = fieldSelector.andQuery(equal("metadata.name",name));
|
||||
}
|
||||
listOptions.setFieldSelector(fieldSelector);
|
||||
return client.listAll(Link.class, listOptions, Sort.by("metadata.creationTimestamp"))
|
||||
.doOnNext(link -> eventPublisher.publishEvent(new RssFeedSyncEvent(this, link,sum,disableSyncList)))
|
||||
.then();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package la.moony.friends.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
public class CommonUtils {
|
||||
|
||||
public static String parseAndTruncateHtml2Text(String html, int length) {
|
||||
if (StringUtils.isBlank(html)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String text = Jsoup.parse(html).text();
|
||||
if (text.length() <= length) {
|
||||
return text;
|
||||
} else {
|
||||
text = text.substring(0, length);
|
||||
text += "...";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package la.moony.friends.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class LinkRequest {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LinkRequest.class);
|
||||
|
||||
private final static String USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
+ "Chrome/58.0.3029.110 Safari/537.3";
|
||||
|
||||
public static String getLinkRss(String linkUrl) {
|
||||
Document document = null;
|
||||
try {
|
||||
document = Jsoup.connect(linkUrl)
|
||||
.followRedirects(true)
|
||||
.maxBodySize(1024 * 1024 * 20)
|
||||
.timeout(10000)
|
||||
.headers(
|
||||
Map.of(
|
||||
"User-Agent", USER_AGENT,
|
||||
"Referer", linkUrl,
|
||||
"Accept", "text/html,application/xhtml+xml,application/xml"
|
||||
)
|
||||
)
|
||||
.get();
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to get link rss ",e);
|
||||
}
|
||||
|
||||
if(document != null) {
|
||||
Element rssElement = document.selectFirst("link[type=application/rss+xml]");
|
||||
if (Objects.nonNull(rssElement)) {
|
||||
return rssElement.absUrl("href");
|
||||
}
|
||||
|
||||
Element atomElement = document.selectFirst("link[type=application/atom+xml]");
|
||||
if (Objects.nonNull(atomElement)) {
|
||||
return atomElement.absUrl("href");
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package la.moony.friends.util;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@UtilityClass
|
||||
public class SortUtils {
|
||||
static final String delimiter = ",";
|
||||
|
||||
/**
|
||||
* <p>Resolve from direction params, e.g. "name,asc" or "name"</p>
|
||||
*
|
||||
* @param directionParams direction params
|
||||
* @return sort object
|
||||
*/
|
||||
public static Sort resolve(List<String> directionParams) {
|
||||
if (CollectionUtils.isEmpty(directionParams)) {
|
||||
return Sort.unsorted();
|
||||
}
|
||||
Sort.Order[] orders = new Sort.Order[directionParams.size()];
|
||||
for (int i = 0; i < directionParams.size(); i++) {
|
||||
String[] parts = directionParams.get(i).split(delimiter);
|
||||
if (parts.length == 1) {
|
||||
orders[i] = new Sort.Order(Sort.Direction.ASC, parts[0]);
|
||||
} else {
|
||||
orders[i] = new Sort.Order(toDirection(parts[1]), parts[0]);
|
||||
}
|
||||
}
|
||||
return Sort.by(orders);
|
||||
}
|
||||
|
||||
private static Sort.Direction toDirection(@NonNull String direction) {
|
||||
Assert.notNull(direction, "Direction must not be null");
|
||||
if (direction.contains(" ")) {
|
||||
throw new IllegalArgumentException("Direction must not contain whitespace");
|
||||
}
|
||||
return Sort.Direction.fromString(direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package la.moony.friends.vo;
|
||||
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class FriendPostVo {
|
||||
|
||||
private MetadataOperator metadata;
|
||||
|
||||
private FriendPost.FriendPostSpec spec;
|
||||
|
||||
public static FriendPostVo from(FriendPost friendPost) {
|
||||
return FriendPostVo.builder()
|
||||
.metadata(friendPost.getMetadata())
|
||||
.spec(friendPost.getSpec())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package la.moony.friends.vo;
|
||||
|
||||
import la.moony.friends.extension.LinkGroup;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public class LinkGroupVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
LinkGroup.LinkGroupSpec spec;
|
||||
|
||||
@With
|
||||
List<LinkVo> links;
|
||||
|
||||
public static LinkGroupVo from(LinkGroup linkGroup) {
|
||||
return LinkGroupVo.builder()
|
||||
.metadata(linkGroup.getMetadata())
|
||||
.spec(linkGroup.getSpec())
|
||||
.links(List.of())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package la.moony.friends.vo;
|
||||
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class LinkVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
Link.LinkSpec spec;
|
||||
|
||||
|
||||
RssFeedSyncLog rssFeedSyncLog;
|
||||
|
||||
List<FriendPostVo> friendPosts;
|
||||
|
||||
public static LinkVo from(Link link) {
|
||||
return LinkVo.builder()
|
||||
.metadata(link.getMetadata())
|
||||
.spec(link.getSpec())
|
||||
.friendPosts(List.of())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package la.moony.friends.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Data
|
||||
public class RssDetail {
|
||||
|
||||
private Channel channel;
|
||||
|
||||
@Data
|
||||
public static class Channel {
|
||||
|
||||
private String title;
|
||||
|
||||
private String link;
|
||||
|
||||
private String description;
|
||||
|
||||
private List<Item> items;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Item {
|
||||
private String title;
|
||||
|
||||
private String link;
|
||||
|
||||
private String author;
|
||||
|
||||
private String description;
|
||||
|
||||
private String uri;
|
||||
|
||||
private Instant pubDate;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: AnnotationSetting
|
||||
metadata:
|
||||
name: annotation-setting-link-rss
|
||||
spec:
|
||||
targetRef:
|
||||
group: core.halo.run
|
||||
kind: Link
|
||||
formSchema:
|
||||
- $formkit: "url"
|
||||
name: "rss_url"
|
||||
label: "RSS 地址"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ReverseProxy
|
||||
metadata:
|
||||
name: friends-static-assets
|
||||
rules:
|
||||
- path: /static/**
|
||||
file:
|
||||
directory: static
|
||||
@@ -0,0 +1,59 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-fridends-anonymous
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
halo.run/hidden: "true"
|
||||
rbac.authorization.halo.run/aggregate-to-anonymous: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "朋友圈"
|
||||
rbac.authorization.halo.run/display-name: "朋友圈匿名接口"
|
||||
rules:
|
||||
- apiGroups: ["api.friend.moony.la"]
|
||||
resources: ["parsingrss","friendposts"]
|
||||
verbs: [ "get", "list" ]
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-friends-view
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "朋友圈"
|
||||
rbac.authorization.halo.run/display-name: "朋友圈查看"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:friends:view"]
|
||||
rules:
|
||||
- apiGroups: [ "friend.moony.la" ]
|
||||
resources: [ "friendposts","cronfriendposts" ]
|
||||
verbs: [ "get", "list" ]
|
||||
- apiGroups: [ "api.friend.moony.la" ]
|
||||
resources: [ "friendposts","parsingrss"]
|
||||
verbs: [ "get", "list" ]
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-friends-manage
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "朋友圈"
|
||||
rbac.authorization.halo.run/display-name: "朋友圈管理"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:friends:manage"]
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
["role-template-friends-view"]
|
||||
rules:
|
||||
- apiGroups: [ "friend.moony.la" ]
|
||||
resources: [ "friendposts","cronfriendposts"]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
- apiGroups: [ "api.friend.moony.la" ]
|
||||
resources: [ "friendposts","syncrssfeed","friendposts/delete"]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: Setting
|
||||
metadata:
|
||||
name: plugin-friends-settings
|
||||
spec:
|
||||
forms:
|
||||
- group: base
|
||||
label: 基本设置
|
||||
formSchema:
|
||||
- $formkit: text
|
||||
label: 页面标题
|
||||
name: title
|
||||
validation: required
|
||||
value: '友链朋友圈'
|
||||
- $formkit: text
|
||||
label: 朋友圈列表显示条数
|
||||
name: pageSize
|
||||
validation: required|Number
|
||||
value: 10
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,25 @@
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: plugin-friends
|
||||
annotations:
|
||||
"store.halo.run/app-id": "app-yISsV"
|
||||
spec:
|
||||
enabled: false
|
||||
requires: ">=2.21.0"
|
||||
pluginDependencies:
|
||||
"PluginLinks": ">=1.5.0"
|
||||
author:
|
||||
name: 困困鱼
|
||||
website: https://github.com/chengzhongxue
|
||||
logo: logo.png
|
||||
homepage: https://www.halo.run/store/apps/app-yISsV
|
||||
repo: https://github.com/chengzhongxue/plugin-friends-new
|
||||
issues: https://github.com/chengzhongxue/plugin-friends-new/issues
|
||||
configMapName: plugin-friends-configMap
|
||||
settingName: plugin-friends-settings
|
||||
displayName: "朋友圈"
|
||||
description: "提供对 RSS 链接的订阅功能,支持获取其订阅内容"
|
||||
license:
|
||||
- name: "GPL-3.0"
|
||||
url: "https://github.com/chengzhongxue/plugin-friends-new/blob/main/LICENSE"
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.color-scheme-auto,
|
||||
[data-color-scheme='auto'] friends-rss {
|
||||
color-scheme: dark;
|
||||
--friends-rss-title-color: #f4f4f5;
|
||||
--friends-rss-description-color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
|
||||
.color-scheme-dark,
|
||||
.dark,
|
||||
[data-color-scheme='dark'] friends-rss {
|
||||
color-scheme: dark;
|
||||
--friends-rss-title-color: #f4f4f5;
|
||||
--friends-rss-description-color: #a1a1aa;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user