feat: expose rss feed endpoint

This commit is contained in:
Tim
2025-08-18 19:15:12 +08:00
parent c84262eb88
commit 6b500466fc
10 changed files with 153 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@@ -31,5 +31,6 @@ public class PostSummaryDto {
private int pointReward;
private PostType type;
private LotteryDto lottery;
private boolean rssExcluded;
}

View File

@@ -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()

View File

@@ -67,4 +67,7 @@ public class Post {
@Column
private LocalDateTime pinnedAt;
@Column(nullable = false)
private boolean rssExcluded = false;
}

View File

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

View File

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