diff --git a/src/main/kotlin/org/hydev/back/controller/CommentController.kt b/src/main/kotlin/org/hydev/back/controller/CommentController.kt index 558c2bb..0d32ca6 100644 --- a/src/main/kotlin/org/hydev/back/controller/CommentController.kt +++ b/src/main/kotlin/org/hydev/back/controller/CommentController.kt @@ -7,8 +7,14 @@ import com.github.kotlintelegrambot.entities.ChatId import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup import com.github.kotlintelegrambot.entities.ParseMode import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.annotation.PreDestroy import org.hydev.back.* import org.hydev.back.ai.HarmLevel import org.hydev.back.ai.IHarmClassifier @@ -24,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.util.HtmlUtils import java.sql.Date +import java.util.concurrent.ConcurrentHashMap import javax.servlet.http.HttpServletRequest @@ -56,6 +63,49 @@ class CommentController( return "✅ 已为评论 #$commentId 添加备注:\n$noteContent" } + private val controllerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @PreDestroy + fun onDestroy() { + controllerScope.cancel("CommentController 正在关闭,取消所有未完成的审核任务") + } + + private val processingCommentIds = ConcurrentHashMap.newKeySet() + + /** + * Acquire the in-process lock for [id], then launch an IO coroutine to run [block]. + * - Lock taken → answer callback with a busy alert and return immediately. + * - Lock acquired → silently acknowledge the callback (stops spinner), run [block] on IO dispatcher. + * Errors inside [block] are caught, logged, and reported to the admin chat. + * The lock is always released in `finally`. + */ + private fun withCommentLock( + bot: com.github.kotlintelegrambot.Bot, + id: Long, + callbackQueryId: String, + chatId: ChatId, + actionName: String, + block: suspend CoroutineScope.() -> Unit + ) { + if (!processingCommentIds.add(id)) { + bot.answerCallbackQuery(callbackQueryId, text = "⚠️ 有人正在处理这条评论,请稍候", showAlert = true) + return + } + bot.answerCallbackQuery(callbackQueryId) + controllerScope.launch { + try { + block() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + System.err.println("[!] Failed to $actionName comment $id: $e") + bot.sendMessage(chatId, "[!] #$id ${actionName}失败: $e") + } finally { + processingCommentIds.remove(id) + } + } + } + private val actionButtons = mapOf( "通过" to "pass", "Spoiler 后通过" to "pass-spoiler", @@ -82,80 +132,82 @@ class CommentController( } if (data == "comment-cancel") { - bot.editMessageReplyMarkup(chatId, msgId, inlId, replyMarkup) + withCommentLock(bot, id, callbackQuery.id, chatId, "cancel") { + bot.editMessageReplyMarkup(chatId, msgId, inlId, replyMarkup) + } return@callback } if (data.startsWith("comment-confirm-")) { val action = data.removePrefix("comment-confirm-") - bot.editMessageReplyMarkup(chatId, msgId, inlId, null) when (action) { // Ban ip - "ban" -> { + "ban" -> withCommentLock(bot, id, callbackQuery.id, chatId, "ban") { val comment = commentRepo.queryById(id)!! val ip = comment.ip - val entry = Ban(ip = ip, reason = "Bad comment #$id") - banRepo.save(entry) - - val escapedPersonId = HtmlUtils.htmlEscape(comment.personId) - val escapedContent = HtmlUtils.htmlEscape(comment.content) - val escapedIp = HtmlUtils.htmlEscape(ip) - val escapedOperator = HtmlUtils.htmlEscape(operator) + banRepo.save(Ban(ip = ip, reason = "Bad comment #$id")) val banMessage = """ - #$id - $escapedPersonId 收到了新的留言: + #$id - ${HtmlUtils.htmlEscape(comment.personId)} 收到了新的留言: -
$escapedContent
+
${HtmlUtils.htmlEscape(comment.content)}
- - 已封禁🚫 $escapedIp by $escapedOperator + - 已封禁🚫 ${HtmlUtils.htmlEscape(ip)} by ${HtmlUtils.htmlEscape(operator)} """.trimIndent() - println("[-] Comment banned! IP $ip banned by $operator due to Comment $id") bot.editMessageText(chatId, msgId, inlId, banMessage, parseMode = ParseMode.HTML) } // Rejected, remove - "reject" -> { + "reject" -> withCommentLock(bot, id, callbackQuery.id, chatId, "reject") { println("[-] Comment rejected! Comment $id deleted by $operator") bot.editMessageText(chatId, msgId, inlId, "$message\n- 已删除❌ by $operator") } // Commit changes - "pass", "pass-spoiler" -> { + "pass", "pass-spoiler" -> withCommentLock(bot, id, callbackQuery.id, chatId, "pass") { var statusMsgId = 0L - bot.sendMessage(chatId, "正在提交更改...").fold( - { statusMsgId = it.messageId }, - { System.err.println("> Failed to send submission message: $it") }) - val comment = commentRepo.queryById(id)!! - - // Spoiler - if (action == "pass-spoiler") - comment.content = "||${comment.content.replace('\n', ' ')}||" - - // Create commit content - val fPath = "people/${comment.personId}/comments/${date("yyyy-MM-dd")}-C${comment.id}.json" - val cMsg = "[+] Comment added by ${comment.submitter} for ${comment.personId}" - - // Build JSON content with optional replies - val content = json("id" to comment.id, "content" to comment.content, - "submitter" to comment.submitter, "date" to comment.date, - *comment.note?.let { arrayOf("replies" to listOf(mapOf("content" to it, "submitter" to "Maintainer"))) } ?: arrayOf()) - println("[+] Comment approved. Adding Comment $id: $content by $operator") - - // Write commit - val url = commitDirectly(comment.submitter, DataEdit(fPath, content), cMsg) - bot.deleteMessage(chatId, statusMsgId) - - // Update database - comment.approved = true - commentRepo.save(comment) - - // Attach URL - bot.editMessageText(chatId, msgId, inlId, "$message\n- 已通过审核✅ by $operator", replyMarkup = - InlineKeyboardMarkup.createSingleRowKeyboard( - InlineKeyboardButton.Url(text = "查看 Commit", url = url) + try { + val comment = commentRepo.queryById(id)!! + if (comment.approved) { + // Already processed (e.g. approved before this server restarted), notify and bail + bot.sendMessage(chatId, "⚠️ 这条评论 (#$id) 已经被处理过了") + return@withCommentLock + } + bot.sendMessage(chatId, "正在提交更改...").fold( + { statusMsgId = it.messageId }, + { System.err.println("> Failed to send submission message: $it") }) + + // Spoiler + if (action == "pass-spoiler") + comment.content = "||${comment.content.replace('\n', ' ')}||" + + // Create commit content + val fPath = "people/${comment.personId}/comments/${date("yyyy-MM-dd")}-C${comment.id}.json" + val cMsg = "[+] Comment added by ${comment.submitter} for ${comment.personId}" + + // Build JSON content with optional replies + val content = json("id" to comment.id, "content" to comment.content, + "submitter" to comment.submitter, "date" to comment.date, + *comment.note?.let { arrayOf("replies" to listOf(mapOf("content" to it, "submitter" to "Maintainer"))) } ?: arrayOf()) + println("[+] Comment approved. Adding Comment $id: $content by $operator") + + // Write commit + val url = commitDirectly(comment.submitter, DataEdit(fPath, content), cMsg) + + // Update database + comment.approved = true + commentRepo.save(comment) + + // Attach URL + bot.editMessageText(chatId, msgId, inlId, "$message\n- 已通过审核✅ by $operator", replyMarkup = + InlineKeyboardMarkup.createSingleRowKeyboard( + InlineKeyboardButton.Url(text = "查看 Commit", url = url) + ) ) - ) + } finally { + if (statusMsgId != 0L) bot.deleteMessage(chatId, statusMsgId) + } } } return@callback @@ -170,7 +222,9 @@ class CommentController( ) } ?: replyMarkup - bot.editMessageReplyMarkup(chatId, msgId, inlId, confirmMarkup) + withCommentLock(bot, id, callbackQuery.id, chatId, "confirm-markup") { + bot.editMessageReplyMarkup(chatId, msgId, inlId, confirmMarkup) + } } @PostMapping("/add")