mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-21 22:41:05 +08:00
feat: expose rss feed endpoint
This commit is contained in:
@@ -119,6 +119,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
||||
@@ -154,7 +155,8 @@ public class SecurityConfig {
|
||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
||||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
||||
uri.startsWith("/api/point-goods") ||
|
||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals"));
|
||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
|
||||
uri.startsWith("/api/rss"));
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
@@ -45,4 +45,14 @@ public class AdminPostController {
|
||||
public PostSummaryDto unpin(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.unpinPost(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/rss-exclude")
|
||||
public PostSummaryDto excludeFromRss(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.excludeFromRss(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/rss-include")
|
||||
public PostSummaryDto includeInRss(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.includeInRss(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.service.PostService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class RssController {
|
||||
private final PostService postService;
|
||||
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
private String websiteUrl;
|
||||
|
||||
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
||||
private static final Pattern HTML_IMAGE = Pattern.compile("<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
||||
|
||||
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
|
||||
public String feed() {
|
||||
List<Post> posts = postService.listLatestRssPosts(10);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
sb.append("<rss version=\"2.0\">");
|
||||
sb.append("<channel>");
|
||||
sb.append("<title>OpenIsle RSS</title>");
|
||||
sb.append("<link>").append(websiteUrl).append("</link>");
|
||||
sb.append("<description>Latest posts</description>");
|
||||
DateTimeFormatter fmt = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
for (Post p : posts) {
|
||||
String link = websiteUrl + "/posts/" + p.getId();
|
||||
sb.append("<item>");
|
||||
sb.append("<title><![CDATA[").append(p.getTitle()).append("]]></title>");
|
||||
sb.append("<link>").append(link).append("</link>");
|
||||
sb.append("<guid isPermaLink=\"true\">").append(link).append("</guid>");
|
||||
sb.append("<pubDate>")
|
||||
.append(p.getCreatedAt().atZone(ZoneId.systemDefault()).format(fmt))
|
||||
.append("</pubDate>");
|
||||
String desc = p.getContent() + "\n<p>更多细节请访问原文:" + link + "</p>";
|
||||
sb.append("<description><![CDATA[").append(desc).append("]]></description>");
|
||||
String img = firstImage(p.getContent());
|
||||
if (img != null) {
|
||||
sb.append("<enclosure url=\"").append(img).append("\" type=\"")
|
||||
.append(getMimeType(img)).append("\" />");
|
||||
}
|
||||
sb.append("</item>");
|
||||
}
|
||||
sb.append("</channel></rss>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String firstImage(String content) {
|
||||
Matcher m = MD_IMAGE.matcher(content);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
m = HTML_IMAGE.matcher(content);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getMimeType(String url) {
|
||||
String lower = url.toLowerCase();
|
||||
if (lower.endsWith(".png")) return "image/png";
|
||||
if (lower.endsWith(".gif")) return "image/gif";
|
||||
return "image/jpeg";
|
||||
}
|
||||
}
|
||||
@@ -31,5 +31,6 @@ public class PostSummaryDto {
|
||||
private int pointReward;
|
||||
private PostType type;
|
||||
private LotteryDto lottery;
|
||||
private boolean rssExcluded;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ public class PostMapper {
|
||||
dto.setCommentCount(commentService.countComments(post.getId()));
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
dto.setRssExcluded(post.isRssExcluded());
|
||||
|
||||
List<ReactionDto> reactions = reactionService.getReactionsForPost(post.getId())
|
||||
.stream()
|
||||
|
||||
@@ -67,4 +67,7 @@ public class Post {
|
||||
@Column
|
||||
private LocalDateTime pinnedAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean rssExcluded = false;
|
||||
|
||||
}
|
||||
|
||||
@@ -106,4 +106,6 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
"WHERE p.createdAt >= :start AND p.createdAt < :end GROUP BY d ORDER BY d")
|
||||
java.util.List<Object[]> countDailyRange(@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
List<Post> findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus status, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,23 @@ public class PostService {
|
||||
this.publishMode = publishMode;
|
||||
}
|
||||
|
||||
public List<Post> listLatestRssPosts(int limit) {
|
||||
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
|
||||
}
|
||||
|
||||
public Post excludeFromRss(Long id) {
|
||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
post.setRssExcluded(true);
|
||||
return postRepository.save(post);
|
||||
}
|
||||
|
||||
public Post includeInRss(Long id) {
|
||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
post.setRssExcluded(false);
|
||||
return postRepository.save(post);
|
||||
}
|
||||
|
||||
public Post createPost(String username,
|
||||
Long categoryId,
|
||||
String title,
|
||||
|
||||
Reference in New Issue
Block a user