mirror of https://github.com/halo-dev/halo
feat: add comment list APIs for theme-side (#2433)
#### What type of PR is this? /kind feature /milestone 2.0 /area core /kind api-change #### What this PR does / why we need it: - 新增主题端评论列表功能 - 新增 `<halo:comment />` html tag 用于扩展评论组件 - 插件可以实现 `CommentWidget` 扩展点来渲染评论组件 html 块,以替换 `<halo:comment />` 标签 #### Which issue(s) this PR fixes: Fixes #2432 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 新增主题端评论列表功能 ```pull/2477/head
parent
73c66289e0
commit
ed8dddbafa
|
@ -43,7 +43,8 @@ public class SwaggerConfig {
|
|||
.group("extension-api")
|
||||
.displayName("Extension APIs")
|
||||
.pathsToMatch("/apis/**")
|
||||
.pathsToExclude("/apis/api.console.halo.run/**", "/apis/api.plugin.halo.run/**")
|
||||
.pathsToExclude("/apis/api.console.halo.run/**", "/apis/api.halo.run/**",
|
||||
"/apis/api.plugin.halo.run/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,15 @@ public class SwaggerConfig {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
GroupedOpenApi customApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("api.halo.run")
|
||||
.displayName("api.halo.run")
|
||||
.pathsToMatch("/apis/api.halo.run/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
GroupedOpenApi pluginCustomApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
|
|
|
@ -68,6 +68,11 @@ public class CommentServiceImpl implements CommentService {
|
|||
return Mono.error(
|
||||
new AccessDeniedException("The comment function has been turned off."));
|
||||
}
|
||||
if (checkCommentOwner(comment, commentSetting.getSystemUserOnly())) {
|
||||
return Mono.error(
|
||||
new AccessDeniedException("Allow system user comments only."));
|
||||
}
|
||||
|
||||
if (comment.getSpec().getTop() == null) {
|
||||
comment.getSpec().setTop(false);
|
||||
}
|
||||
|
@ -76,7 +81,7 @@ public class CommentServiceImpl implements CommentService {
|
|||
}
|
||||
comment.getSpec()
|
||||
.setApproved(Boolean.FALSE.equals(commentSetting.getRequireReviewForNew()));
|
||||
comment.getSpec().setHidden(comment.getSpec().getApproved());
|
||||
comment.getSpec().setHidden(!comment.getSpec().getApproved());
|
||||
if (comment.getSpec().getOwner() != null) {
|
||||
return Mono.just(comment);
|
||||
}
|
||||
|
@ -93,6 +98,14 @@ public class CommentServiceImpl implements CommentService {
|
|||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
private boolean checkCommentOwner(Comment comment, Boolean onlySystemUser) {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
if (Boolean.TRUE.equals(onlySystemUser)) {
|
||||
return owner != null && Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Comment.CommentOwner toCommentOwner(User user) {
|
||||
Comment.CommentOwner owner = new Comment.CommentOwner();
|
||||
owner.setKind(User.KIND);
|
||||
|
@ -122,6 +135,7 @@ public class CommentServiceImpl implements CommentService {
|
|||
builder.subject(subject);
|
||||
return builder;
|
||||
})
|
||||
.switchIfEmpty(Mono.just(builder))
|
||||
)
|
||||
.map(ListedComment.ListedCommentBuilder::build);
|
||||
}
|
||||
|
|
|
@ -56,9 +56,12 @@ public class ReplyServiceImpl implements ReplyService {
|
|||
throw new AccessDeniedException(
|
||||
"The comment function has been turned off.");
|
||||
}
|
||||
if (checkReplyOwner(reply, commentSetting.getSystemUserOnly())) {
|
||||
throw new AccessDeniedException("Allow system user reply only.");
|
||||
}
|
||||
reply.getSpec().setApproved(
|
||||
Boolean.FALSE.equals(commentSetting.getRequireReviewForNew()));
|
||||
reply.getSpec().setHidden(reply.getSpec().getApproved());
|
||||
reply.getSpec().setHidden(!reply.getSpec().getApproved());
|
||||
return reply;
|
||||
});
|
||||
})
|
||||
|
@ -82,6 +85,14 @@ public class ReplyServiceImpl implements ReplyService {
|
|||
);
|
||||
}
|
||||
|
||||
private boolean checkReplyOwner(Reply reply, Boolean onlySystemUser) {
|
||||
Comment.CommentOwner owner = reply.getSpec().getOwner();
|
||||
if (Boolean.TRUE.equals(onlySystemUser)) {
|
||||
return owner != null && Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedReply>> list(ReplyQuery query) {
|
||||
return client.list(Reply.class, getReplyPredicate(query), defaultComparator(),
|
||||
|
|
|
@ -124,7 +124,13 @@ public class CommentEndpoint implements CustomEndpoint {
|
|||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
}
|
||||
|
||||
private String userAgentFrom(ServerRequest request) {
|
||||
/**
|
||||
* Gets user-agent from server request.
|
||||
*
|
||||
* @param request server request
|
||||
* @return user-agent string if found, otherwise "unknown"
|
||||
*/
|
||||
public static String userAgentFrom(ServerRequest request) {
|
||||
HttpHeaders httpHeaders = request.headers().asHttpHeaders();
|
||||
// https://en.wikipedia.org/wiki/User_agent
|
||||
String userAgent = httpHeaders.getFirst(HttpHeaders.USER_AGENT);
|
||||
|
@ -132,6 +138,6 @@ public class CommentEndpoint implements CustomEndpoint {
|
|||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
||||
userAgent = httpHeaders.getFirst("Sec-CH-UA");
|
||||
}
|
||||
return StringUtils.defaultString(userAgent, "UNKNOWN");
|
||||
return StringUtils.defaultString(userAgent, "unknown");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import org.thymeleaf.spring6.context.SpringContextUtils;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Comment element tag processor.</p>
|
||||
* <p>Replace the comment tag <code><halo:comment /></code> with the given content.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CommentElementTagProcessor extends AbstractElementTagProcessor {
|
||||
|
||||
private static final String TAG_NAME = "comment";
|
||||
|
||||
private static final int PRECEDENCE = 1000;
|
||||
|
||||
/**
|
||||
* Constructor footer element tag processor with HTML mode, dialect prefix, comment tag name.
|
||||
*
|
||||
* @param dialectPrefix dialect prefix
|
||||
*/
|
||||
public CommentElementTagProcessor(final String dialectPrefix) {
|
||||
super(
|
||||
TemplateMode.HTML, // This processor will apply only to HTML mode
|
||||
dialectPrefix, // Prefix to be applied to name for matching
|
||||
TAG_NAME, // Tag name: match specifically this tag
|
||||
true, // Apply dialect prefix to tag name
|
||||
null, // No attribute name: will match by tag name
|
||||
false, // No prefix to be applied to attribute name
|
||||
PRECEDENCE); // Precedence (inside dialect's own precedence)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler) {
|
||||
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
|
||||
ExtensionComponentsFinder componentsFinder =
|
||||
appCtx.getBean(ExtensionComponentsFinder.class);
|
||||
List<CommentWidget> commentWidgets = componentsFinder.getExtensions(CommentWidget.class);
|
||||
if (commentWidgets.isEmpty()) {
|
||||
structureHandler.replaceWith("", false);
|
||||
return;
|
||||
}
|
||||
// TODO if find more than one comment widget, query CommentWidget setting to decide which
|
||||
// one to use.
|
||||
commentWidgets.get(0).render(context, tag, structureHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
|
||||
/**
|
||||
* Comment widget extension point to extend the <halo:comment /> tag of the theme-side.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface CommentWidget extends ExtensionPoint {
|
||||
|
||||
void render(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler);
|
||||
}
|
|
@ -28,6 +28,7 @@ public class HaloProcessorDialect extends AbstractProcessorDialect {
|
|||
processors.add(new GlobalHeadInjectionProcessor(dialectPrefix));
|
||||
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
|
||||
processors.add(new JsonNodePropertyAccessorBoundariesProcessor());
|
||||
processors.add(new CommentElementTagProcessor(dialectPrefix));
|
||||
return processors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
package run.halo.app.theme.endpoint;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
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 reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.content.comment.CommentRequest;
|
||||
import run.halo.app.content.comment.CommentService;
|
||||
import run.halo.app.content.comment.ReplyRequest;
|
||||
import run.halo.app.content.comment.ReplyService;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.core.extension.endpoint.CommentEndpoint;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
import run.halo.app.infra.utils.IpAddressUtils;
|
||||
import run.halo.app.theme.finders.CommentFinder;
|
||||
import run.halo.app.theme.finders.vo.CommentVo;
|
||||
import run.halo.app.theme.finders.vo.ReplyVo;
|
||||
|
||||
/**
|
||||
* Endpoint for {@link CommentFinder}.
|
||||
*/
|
||||
@Component
|
||||
public class CommentFinderEndpoint implements CustomEndpoint {
|
||||
|
||||
private final CommentFinder commentFinder;
|
||||
private final CommentService commentService;
|
||||
private final ReplyService replyService;
|
||||
|
||||
/**
|
||||
* Construct a {@link CommentFinderEndpoint} instance.
|
||||
*
|
||||
* @param commentFinder comment finder
|
||||
* @param commentService comment service to create comment
|
||||
* @param replyService reply service to create reply
|
||||
*/
|
||||
public CommentFinderEndpoint(CommentFinder commentFinder, CommentService commentService,
|
||||
ReplyService replyService) {
|
||||
this.commentFinder = commentFinder;
|
||||
this.commentService = commentService;
|
||||
this.replyService = replyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.halo.run/v1alpha1/Comment";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.POST("comments", this::createComment,
|
||||
builder -> builder.operationId("CreateComment")
|
||||
.description("Create a comment.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(CommentRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Comment.class))
|
||||
)
|
||||
.POST("comments/{name}/reply", this::createReply,
|
||||
builder -> builder.operationId("CreateReply")
|
||||
.description("Create a reply.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(ReplyRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Reply.class))
|
||||
)
|
||||
.GET("comments", this::listComments, builder -> {
|
||||
builder.operationId("ListComments")
|
||||
.description("List comments.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(CommentVo.class))
|
||||
);
|
||||
QueryParamBuildUtil.buildParametersFromType(builder, CommentQuery.class);
|
||||
})
|
||||
.GET("comments/{name}", this::getComment, builder -> {
|
||||
builder.operationId("GetComment")
|
||||
.description("Get a comment.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(CommentVo.class))
|
||||
);
|
||||
})
|
||||
.GET("comments/{name}/reply", this::listCommentReplies, builder -> {
|
||||
builder.operationId("ListCommentReplies")
|
||||
.description("List comment replies.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(ReplyVo.class))
|
||||
);
|
||||
QueryParamBuildUtil.buildParametersFromType(builder, PageableRequest.class);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("api.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
Mono<ServerResponse> createComment(ServerRequest request) {
|
||||
return request.bodyToMono(CommentRequest.class)
|
||||
.flatMap(commentRequest -> {
|
||||
Comment comment = commentRequest.toComment();
|
||||
comment.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
comment.getSpec().setUserAgent(CommentEndpoint.userAgentFrom(request));
|
||||
return commentService.create(comment);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> createReply(ServerRequest request) {
|
||||
String commentName = request.pathVariable("name");
|
||||
return request.bodyToMono(ReplyRequest.class)
|
||||
.flatMap(replyRequest -> {
|
||||
Reply reply = replyRequest.toReply();
|
||||
reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
reply.getSpec().setUserAgent(CommentEndpoint.userAgentFrom(request));
|
||||
return replyService.create(commentName, reply);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> listComments(ServerRequest request) {
|
||||
CommentQuery commentQuery = new CommentQuery(request.queryParams());
|
||||
return Mono.defer(() -> Mono.just(
|
||||
commentFinder.list(commentQuery.toRef(), commentQuery.getPage(),
|
||||
commentQuery.getSize())))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> getComment(ServerRequest request) {
|
||||
String name = request.pathVariable("name");
|
||||
return Mono.defer(() -> Mono.justOrEmpty(commentFinder.getByName(name)))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> listCommentReplies(ServerRequest request) {
|
||||
String commentName = request.pathVariable("name");
|
||||
IListRequest.QueryListRequest queryParams =
|
||||
new IListRequest.QueryListRequest(request.queryParams());
|
||||
return Mono.defer(() -> Mono.just(
|
||||
commentFinder.listReply(commentName, queryParams.getPage(), queryParams.getSize())))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
public static class CommentQuery extends PageableRequest {
|
||||
|
||||
public CommentQuery(MultiValueMap<String, String> queryParams) {
|
||||
super(queryParams);
|
||||
}
|
||||
|
||||
@Schema(description = "The comment subject group.")
|
||||
public String getGroup() {
|
||||
return queryParams.getFirst("group");
|
||||
}
|
||||
|
||||
@Schema(required = true, description = "The comment subject version.")
|
||||
public String getVersion() {
|
||||
return emptyToNull(queryParams.getFirst("version"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Ref}s kind.
|
||||
*
|
||||
* @return comment subject ref kind
|
||||
*/
|
||||
@Schema(required = true, description = "The comment subject kind.")
|
||||
public String getKind() {
|
||||
String kind = emptyToNull(queryParams.getFirst("kind"));
|
||||
if (kind == null) {
|
||||
throw new IllegalArgumentException("The kind must not be null.");
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Ref}s name.
|
||||
*
|
||||
* @return comment subject ref name
|
||||
*/
|
||||
@Schema(required = true, description = "The comment subject name.")
|
||||
public String getName() {
|
||||
String name = emptyToNull(queryParams.getFirst("name"));
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("The name must not be null.");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
Ref toRef() {
|
||||
Ref ref = new Ref();
|
||||
ref.setGroup(getGroup());
|
||||
ref.setKind(getKind());
|
||||
ref.setVersion(getVersion());
|
||||
ref.setName(getName());
|
||||
return ref;
|
||||
}
|
||||
|
||||
String emptyToNull(String str) {
|
||||
return StringUtils.isBlank(str) ? null : str;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PageableRequest extends IListRequest.QueryListRequest {
|
||||
|
||||
public PageableRequest(MultiValueMap<String, String> queryParams) {
|
||||
super(queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<String> getLabelSelector() {
|
||||
throw new UnsupportedOperationException("Unsupported this parameter");
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<String> getFieldSelector() {
|
||||
throw new UnsupportedOperationException("Unsupported this parameter");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,14 +2,17 @@ package run.halo.app.theme.finders.impl;
|
|||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.comment.OwnerInfo;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
|
@ -36,7 +39,7 @@ public class CommentFinderImpl implements CommentFinder {
|
|||
@Override
|
||||
public CommentVo getByName(String name) {
|
||||
return client.fetch(Comment.class, name)
|
||||
.map(CommentVo::from)
|
||||
.flatMap(this::toCommentVo)
|
||||
.block();
|
||||
}
|
||||
|
||||
|
@ -45,11 +48,13 @@ public class CommentFinderImpl implements CommentFinder {
|
|||
return client.list(Comment.class, fixedPredicate(ref),
|
||||
defaultComparator(),
|
||||
pageNullSafe(page), sizeNullSafe(size))
|
||||
.map(list -> {
|
||||
List<CommentVo> commentVos = list.get().map(CommentVo::from).toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
commentVos);
|
||||
})
|
||||
.flatMap(list -> Flux.fromStream(list.get().map(this::toCommentVo))
|
||||
.flatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(commentVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
commentVos)
|
||||
)
|
||||
)
|
||||
.block();
|
||||
}
|
||||
|
||||
|
@ -62,14 +67,39 @@ public class CommentFinderImpl implements CommentFinder {
|
|||
&& Objects.equals(false, reply.getSpec().getHidden())
|
||||
&& Objects.equals(true, reply.getSpec().getApproved()),
|
||||
comparator.reversed(), pageNullSafe(page), sizeNullSafe(size))
|
||||
.map(list -> {
|
||||
List<ReplyVo> replyVos = list.get().map(ReplyVo::from).toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
replyVos);
|
||||
})
|
||||
.flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo))
|
||||
.flatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(replyVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
replyVos))
|
||||
)
|
||||
.block();
|
||||
}
|
||||
|
||||
private Mono<CommentVo> toCommentVo(Comment comment) {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
return Mono.just(CommentVo.from(comment))
|
||||
.flatMap(commentVo -> getOwnerInfo(owner)
|
||||
.map(commentVo::withOwner)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ReplyVo> toReplyVo(Reply reply) {
|
||||
return Mono.just(ReplyVo.from(reply))
|
||||
.flatMap(replyVo -> getOwnerInfo(reply.getSpec().getOwner())
|
||||
.map(replyVo::withOwner)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {
|
||||
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
|
||||
return Mono.just(OwnerInfo.from(owner));
|
||||
}
|
||||
return client.fetch(User.class, owner.getName())
|
||||
.map(OwnerInfo::from)
|
||||
.switchIfEmpty(Mono.just(OwnerInfo.ghostUser()));
|
||||
}
|
||||
|
||||
private Predicate<Comment> fixedPredicate(Ref ref) {
|
||||
Assert.notNull(ref, "Comment subject reference must not be null");
|
||||
return comment -> comment.getSpec().getSubjectRef().equals(ref)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import run.halo.app.content.comment.OwnerInfo;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
|
||||
|
@ -17,10 +20,18 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@EqualsAndHashCode
|
||||
public class CommentVo {
|
||||
|
||||
@Schema(required = true)
|
||||
MetadataOperator metadata;
|
||||
|
||||
@Schema(required = true)
|
||||
Comment.CommentSpec spec;
|
||||
|
||||
Comment.CommentStatus status;
|
||||
|
||||
@With
|
||||
@Schema(required = true)
|
||||
OwnerInfo owner;
|
||||
|
||||
/**
|
||||
* Convert {@link Comment} to {@link CommentVo}.
|
||||
*
|
||||
|
@ -31,6 +42,7 @@ public class CommentVo {
|
|||
return CommentVo.builder()
|
||||
.metadata(comment.getMetadata())
|
||||
.spec(comment.getSpec())
|
||||
.status(comment.getStatus())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import run.halo.app.content.comment.OwnerInfo;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
|
||||
|
@ -19,10 +22,16 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@EqualsAndHashCode
|
||||
public class ReplyVo {
|
||||
|
||||
@Schema(required = true)
|
||||
MetadataOperator metadata;
|
||||
|
||||
@Schema(required = true)
|
||||
Reply.ReplySpec spec;
|
||||
|
||||
@With
|
||||
@Schema(required = true)
|
||||
OwnerInfo owner;
|
||||
|
||||
/**
|
||||
* Convert {@link Reply} to {@link ReplyVo}.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,6 @@ metadata:
|
|||
halo.run/role-template: "true"
|
||||
halo.run/hidden: "true"
|
||||
rules:
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
- apiGroups: [ "api.halo.run" ]
|
||||
resources: [ "comments", "comments/reply" ]
|
||||
verbs: [ "create" ]
|
||||
verbs: [ "create", "get", "list" ]
|
||||
|
|
|
@ -141,7 +141,7 @@ class CommentServiceImplTest {
|
|||
"top": false,
|
||||
"allowNotification": true,
|
||||
"approved": false,
|
||||
"hidden": false,
|
||||
"hidden": true,
|
||||
"subjectRef": {
|
||||
"group": "content.halo.run",
|
||||
"version": "v1alpha1",
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
|
||||
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
|
||||
import org.thymeleaf.templateresolver.StringTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import org.thymeleaf.templateresource.StringTemplateResource;
|
||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommentElementTagProcessor}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see ExtensionComponentsFinder
|
||||
* @see HaloProcessorDialect
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommentElementTagProcessorTest {
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private ExtensionComponentsFinder componentsFinder;
|
||||
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
|
||||
templateEngine = new TemplateEngine();
|
||||
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
|
||||
templateEngine.addTemplateResolver(new TestTemplateResolver());
|
||||
when(applicationContext.getBean(eq(ExtensionComponentsFinder.class)))
|
||||
.thenReturn(componentsFinder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doProcess() {
|
||||
Context context = getContext();
|
||||
|
||||
String result = templateEngine.process("commentWidget", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<p>comment widget:</p>
|
||||
\s
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
|
||||
when(componentsFinder.getExtensions(CommentWidget.class))
|
||||
.thenReturn(List.of(new DefaultCommentWidget()));
|
||||
result = templateEngine.process("commentWidget", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<p>comment widget:</p>
|
||||
<p>Comment in default widget</p>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
}
|
||||
|
||||
static class DefaultCommentWidget implements CommentWidget {
|
||||
|
||||
@Override
|
||||
public void render(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler) {
|
||||
structureHandler.replaceWith("<p>Comment in default widget</p>", false);
|
||||
}
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = new Context();
|
||||
context.setVariable(
|
||||
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
|
||||
new ThymeleafEvaluationContext(applicationContext, null));
|
||||
return context;
|
||||
}
|
||||
|
||||
static class TestTemplateResolver extends StringTemplateResolver {
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,
|
||||
String ownerTemplate, String template,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
if (template.equals("commentWidget")) {
|
||||
return new StringTemplateResource(commentWidget());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String commentWidget() {
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<p>comment widget:</p>
|
||||
<halo:comment/>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package run.halo.app.theme.endpoint;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.comment.CommentRequest;
|
||||
import run.halo.app.content.comment.CommentService;
|
||||
import run.halo.app.content.comment.ReplyRequest;
|
||||
import run.halo.app.content.comment.ReplyService;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.theme.finders.CommentFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommentFinderEndpoint}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommentFinderEndpointTest {
|
||||
@Mock
|
||||
private CommentFinder commentFinder;
|
||||
|
||||
@Mock
|
||||
private CommentService commentService;
|
||||
|
||||
@Mock
|
||||
private ReplyService replyService;
|
||||
|
||||
@InjectMocks
|
||||
private CommentFinderEndpoint commentFinderEndpoint;
|
||||
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
webTestClient = WebTestClient
|
||||
.bindToRouterFunction(commentFinderEndpoint.endpoint())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listComments() {
|
||||
when(commentFinder.list(any(), anyInt(), anyInt()))
|
||||
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
|
||||
|
||||
Ref ref = new Ref();
|
||||
ref.setGroup("content.halo.run");
|
||||
ref.setVersion("v1alpha1");
|
||||
ref.setKind("Post");
|
||||
ref.setName("test");
|
||||
webTestClient.get()
|
||||
.uri(uriBuilder -> {
|
||||
return uriBuilder.path("/comments")
|
||||
.queryParam("group", ref.getGroup())
|
||||
.queryParam("version", ref.getVersion())
|
||||
.queryParam("kind", ref.getKind())
|
||||
.queryParam("name", ref.getName())
|
||||
.queryParam("page", 1)
|
||||
.queryParam("size", 10)
|
||||
.build();
|
||||
})
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);
|
||||
verify(commentFinder, times(1)).list(refCaptor.capture(), eq(1), eq(10));
|
||||
Ref value = refCaptor.getValue();
|
||||
assertThat(value).isEqualTo(ref);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getComment() {
|
||||
when(commentFinder.getByName(any()))
|
||||
.thenReturn(null);
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/comments/test-comment")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
verify(commentFinder, times(1)).getByName(eq("test-comment"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listCommentReplies() {
|
||||
when(commentFinder.listReply(any(), anyInt(), anyInt()))
|
||||
.thenReturn(new ListResult<>(2, 20, 0, List.of()));
|
||||
|
||||
webTestClient.get()
|
||||
.uri(uriBuilder -> {
|
||||
return uriBuilder.path("/comments/test-comment/reply")
|
||||
.queryParam("page", 2)
|
||||
.queryParam("size", 20)
|
||||
.build();
|
||||
})
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
verify(commentFinder, times(1)).listReply(eq("test-comment"), eq(2), eq(20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createComment() {
|
||||
when(commentService.create(any())).thenReturn(Mono.empty());
|
||||
|
||||
final CommentRequest commentRequest = new CommentRequest();
|
||||
Ref ref = new Ref();
|
||||
ref.setGroup("content.halo.run");
|
||||
ref.setVersion("v1alpha1");
|
||||
ref.setKind("Post");
|
||||
ref.setName("test-post");
|
||||
commentRequest.setSubjectRef(ref);
|
||||
commentRequest.setContent("content");
|
||||
commentRequest.setRaw("raw");
|
||||
commentRequest.setAllowNotification(false);
|
||||
webTestClient.post()
|
||||
.uri("/comments")
|
||||
.bodyValue(commentRequest)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
|
||||
verify(commentService, times(1)).create(captor.capture());
|
||||
Comment value = captor.getValue();
|
||||
assertThat(value.getSpec().getIpAddress()).isNotNull();
|
||||
assertThat(value.getSpec().getUserAgent()).isNotNull();
|
||||
assertThat(value.getSpec().getSubjectRef()).isEqualTo(ref);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReply() {
|
||||
when(replyService.create(any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
final ReplyRequest replyRequest = new ReplyRequest();
|
||||
replyRequest.setRaw("raw");
|
||||
replyRequest.setContent("content");
|
||||
replyRequest.setAllowNotification(true);
|
||||
|
||||
webTestClient.post()
|
||||
.uri("/comments/test-comment/reply")
|
||||
.bodyValue(replyRequest)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
ArgumentCaptor<Reply> captor = ArgumentCaptor.forClass(Reply.class);
|
||||
verify(replyService, times(1)).create(eq("test-comment"), captor.capture());
|
||||
Reply value = captor.getValue();
|
||||
assertThat(value.getSpec().getIpAddress()).isNotNull();
|
||||
assertThat(value.getSpec().getUserAgent()).isNotNull();
|
||||
assertThat(value.getSpec().getQuoteReply()).isNull();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue