1#![cfg(feature = "server")]
9
10use rmcp::handler::server::tool::Extension;
11use rmcp::handler::server::wrapper::Parameters;
12use rmcp::model::CallToolResult;
13use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
14use serde::Deserialize;
15
16use super::common::{internal, ok_json, require_scope};
17use crate::models::mcp_token::TokenScope;
18
19#[tool_router(router = comments_router, vis = "pub")]
20impl crate::mcp::server::YggMcpServer {
21 #[tool(
23 description = "列出全部评论(分页,每页 20 条)。可按状态筛选:pending/approved/spam/trash。"
24 )]
25 async fn list_comments(
26 &self,
27 Parameters(p): Parameters<ListCommentsParams>,
28 Extension(parts): Extension<http::request::Parts>,
29 ) -> Result<CallToolResult, McpError> {
30 let _principal = require_scope(&parts, "list_comments", TokenScope::Write)?;
31
32 let page = p.page.unwrap_or(1).max(1);
33 let resp = crate::api::comments::list_admin_comments_impl(page, p.status.clone())
34 .await
35 .map_err(|e| internal(format!("{e:?}"), "list comments"))?;
36
37 let comments: Vec<CommentItem> = resp
39 .comments
40 .into_iter()
41 .map(|c| CommentItem {
42 id: c.id,
43 post_id: c.post_id,
44 post_title: c.post_title,
45 post_slug: c.post_slug,
46 parent_id: c.parent_id,
47 depth: c.depth,
48 author_name: c.author_name,
49 author_url: c.author_url,
50 content_md: c.content_md,
51 status: c.status,
52 created_at: c.created_at.to_rfc3339(),
53 })
54 .collect();
55
56 ok_json(CommentsList {
57 comments,
58 total: resp.total,
59 page,
60 per_page: 20,
61 })
62 }
63
64 #[tool(description = "通过指定评论。同时递归通过所有 pending 的祖先评论,确保嵌套链可见。")]
66 async fn approve_comment(
67 &self,
68 Parameters(p): Parameters<CommentIdParams>,
69 Extension(parts): Extension<http::request::Parts>,
70 ) -> Result<CallToolResult, McpError> {
71 let _principal = require_scope(&parts, "approve_comment", TokenScope::Write)?;
72
73 let found = crate::api::comments::approve_comment_impl(p.comment_id)
74 .await
75 .map_err(|e| internal(format!("{e:?}"), "approve comment"))?;
76 if !found {
77 return Err(McpError::invalid_request("评论不存在", None));
78 }
79
80 ok_json(CommentResult {
81 success: true,
82 message: "已通过".into(),
83 })
84 }
85
86 #[tool(description = "删除指定评论(移入回收站,软删除)。")]
88 async fn delete_comment(
89 &self,
90 Parameters(p): Parameters<CommentIdParams>,
91 Extension(parts): Extension<http::request::Parts>,
92 ) -> Result<CallToolResult, McpError> {
93 let _principal = require_scope(&parts, "delete_comment", TokenScope::Write)?;
94
95 crate::api::comments::trash_comment_impl(p.comment_id)
96 .await
97 .map_err(|e| internal(format!("{e:?}"), "trash comment"))?;
98
99 ok_json(CommentResult {
100 success: true,
101 message: "已删除".into(),
102 })
103 }
104
105 #[tool(description = "设置评论审核状态。status 可选 approved/spam/trash。trash 会软删除。")]
107 async fn set_comment_status(
108 &self,
109 Parameters(p): Parameters<SetCommentStatusParams>,
110 Extension(parts): Extension<http::request::Parts>,
111 ) -> Result<CallToolResult, McpError> {
112 let _principal = require_scope(&parts, "set_comment_status", TokenScope::Write)?;
113
114 let normalized = p.status.trim().to_lowercase();
115 if !matches!(normalized.as_str(), "approved" | "spam" | "trash") {
116 return Err(McpError::invalid_request(
117 "status must be one of: approved, spam, trash",
118 None,
119 ));
120 }
121
122 let found = match normalized.as_str() {
123 "approved" => crate::api::comments::approve_comment_impl(p.comment_id).await,
125 "spam" => crate::api::comments::spam_comment_impl(p.comment_id).await,
126 "trash" => crate::api::comments::trash_comment_impl(p.comment_id).await,
127 _ => unreachable!("validated above"),
128 }
129 .map_err(|e| internal(format!("{e:?}"), "set comment status"))?;
130 if !found {
131 return Err(McpError::invalid_request("评论不存在", None));
132 }
133
134 ok_json(CommentResult {
135 success: true,
136 message: format!("状态已设为 {normalized}"),
137 })
138 }
139}
140
141#[derive(Debug, Deserialize, schemars::JsonSchema)]
146pub struct ListCommentsParams {
147 #[serde(default)]
149 pub page: Option<i32>,
150 #[serde(default)]
152 pub status: Option<String>,
153}
154
155#[derive(Debug, Deserialize, schemars::JsonSchema)]
156pub struct CommentIdParams {
157 pub comment_id: i64,
159}
160
161#[derive(Debug, Deserialize, schemars::JsonSchema)]
162pub struct SetCommentStatusParams {
163 pub comment_id: i64,
165 pub status: String,
167}
168
169#[derive(Debug, serde::Serialize)]
170struct CommentItem {
171 id: i64,
172 post_id: i32,
173 post_title: String,
174 post_slug: String,
175 parent_id: Option<i64>,
176 depth: i32,
177 author_name: String,
178 author_url: Option<String>,
179 content_md: String,
180 status: crate::models::comment::CommentStatus,
181 created_at: String,
182}
183
184#[derive(Debug, serde::Serialize)]
185struct CommentsList {
186 comments: Vec<CommentItem>,
187 total: i64,
188 page: i32,
189 per_page: i64,
190}
191
192#[derive(Debug, serde::Serialize)]
193struct CommentResult {
194 success: bool,
195 message: String,
196}