feat: 添加Gitea WebHook模块
This commit is contained in:
parent
7c9e1b5b9a
commit
3f3196e5ac
21
.idea/claudeCodeTabState.xml
Normal file
21
.idea/claudeCodeTabState.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeCodeTabState">
|
||||
<option name="tabSessions">
|
||||
<map>
|
||||
<entry key="0">
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="15d84e94-4c9e-4c16-9632-6aa5b1977df6" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="claude-sonnet-4-6[1m]" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -124,6 +124,7 @@ data class ModuleConfig(
|
|||
RCON_PLAYER_LIST_MODULE(Modules.RCON_PLAYER_LIST),
|
||||
STATE_MODULE(Modules.STATE),
|
||||
HELP_MODULE(Modules.HELP),
|
||||
GITEA_WEBHOOK_MODULE(Modules.GITEA_WEBHOOK),
|
||||
UNKNOWN_MODULE("UnknownModule");
|
||||
}
|
||||
// 基础获取方法
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import top.r3944realms.ltdmanager.core.config.ModuleConfig
|
|||
import top.r3944realms.ltdmanager.core.config.ModuleConfig.Module.ModuleType.*
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.module.exception.ConfigError
|
||||
import top.r3944realms.ltdmanager.module.gitea.GiteaEventType
|
||||
import top.r3944realms.ltdmanager.module.gitea.GiteaWebhookModule
|
||||
|
||||
object ModuleFactory {
|
||||
fun createModule(config: ModuleConfig.Module): BaseModule {
|
||||
|
|
@ -20,6 +22,7 @@ object ModuleFactory {
|
|||
STATE_MODULE -> createState(config)
|
||||
MOD_GROUP_HANDLER_MODULE -> createModGroupHandler(config)
|
||||
HELP_MODULE -> createHelpModule(config)
|
||||
GITEA_WEBHOOK_MODULE -> createGiteaWebhook(config)
|
||||
UNKNOWN_MODULE -> throw ConfigError(ConfigError.Type.INVALID_PARAMETER, "unknown module")
|
||||
}
|
||||
}
|
||||
|
|
@ -190,4 +193,25 @@ object ModuleFactory {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createGiteaWebhook(config: ModuleConfig.Module): GiteaWebhookModule {
|
||||
val port = config.int("webhook-port")
|
||||
val webhookPath = config.getOrDefault("webhook-path", "/gitea-webhook")
|
||||
val secret = config.getOrDefault("webhook-secret", "")
|
||||
val targetGroupId = config.long("target-group-id")
|
||||
val eventNames = config.getOrDefault("events", emptyList<String>())
|
||||
val enabledEvents = if (eventNames.isEmpty()) {
|
||||
GiteaEventType.entries.toSet()
|
||||
} else {
|
||||
eventNames.mapNotNull { GiteaEventType.fromHeader(it) }.toSet()
|
||||
}
|
||||
return GiteaWebhookModule(
|
||||
config.name,
|
||||
port,
|
||||
webhookPath,
|
||||
secret,
|
||||
targetGroupId,
|
||||
enabledEvents
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ object Modules {
|
|||
val RCON_PLAYER_LIST: String = register("RconPlayerListModule")
|
||||
val INVITATION_CODE: String = register("InvitationCodeModule")
|
||||
val STATE: String = register("StateModule")
|
||||
val GITEA_WEBHOOK: String = register("GiteaWebhookModule")
|
||||
fun register(name: String): String {
|
||||
MODULES.add(name)
|
||||
return name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package top.r3944realms.ltdmanager.module.gitea
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GiteaUser(
|
||||
val id: Long = 0,
|
||||
val login: String = "",
|
||||
@SerialName("login_name") val loginName: String = "",
|
||||
@SerialName("full_name") val fullName: String = "",
|
||||
val email: String = "",
|
||||
@SerialName("avatar_url") val avatarUrl: String = "",
|
||||
val username: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaRepository(
|
||||
val id: Long = 0,
|
||||
val name: String = "",
|
||||
@SerialName("full_name") val fullName: String = "",
|
||||
val owner: GiteaUser = GiteaUser(),
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val description: String = "",
|
||||
@SerialName("ssh_url") val sshUrl: String = "",
|
||||
@SerialName("clone_url") val cloneUrl: String = "",
|
||||
@SerialName("default_branch") val defaultBranch: String = "",
|
||||
val private: Boolean = false,
|
||||
val website: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaCommit(
|
||||
val id: String = "",
|
||||
val message: String = "",
|
||||
val url: String = "",
|
||||
val author: GiteaUser = GiteaUser(),
|
||||
val committer: GiteaUser = GiteaUser(),
|
||||
val timestamp: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaIssue(
|
||||
val id: Long = 0,
|
||||
val number: Long = 0,
|
||||
val title: String = "",
|
||||
val body: String = "",
|
||||
val state: String = "",
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val user: GiteaUser = GiteaUser(),
|
||||
val assignee: GiteaUser? = null,
|
||||
@SerialName("is_pull") val isPull: Boolean = false,
|
||||
val comments: Long = 0,
|
||||
@SerialName("created_at") val createdAt: String = "",
|
||||
@SerialName("updated_at") val updatedAt: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaPullRequest(
|
||||
val id: Long = 0,
|
||||
val number: Long = 0,
|
||||
val title: String = "",
|
||||
val body: String = "",
|
||||
val state: String = "",
|
||||
val merged: Boolean = false,
|
||||
@SerialName("mergeable") val mergeable: Boolean? = null,
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val user: GiteaUser = GiteaUser(),
|
||||
@SerialName("head") val headBranch: GiteaBranchRef = GiteaBranchRef(),
|
||||
@SerialName("base") val baseBranch: GiteaBranchRef = GiteaBranchRef(),
|
||||
@SerialName("merged_by") val mergedBy: GiteaUser? = null,
|
||||
@SerialName("created_at") val createdAt: String = "",
|
||||
@SerialName("updated_at") val updatedAt: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaBranchRef(
|
||||
val label: String = "",
|
||||
val ref: String = "",
|
||||
val sha: String = "",
|
||||
val repo: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaRelease(
|
||||
val id: Long = 0,
|
||||
@SerialName("tag_name") val tagName: String = "",
|
||||
@SerialName("target_commitish") val targetCommitish: String = "",
|
||||
val name: String = "",
|
||||
val body: String = "",
|
||||
val draft: Boolean = false,
|
||||
val prerelease: Boolean = false,
|
||||
val url: String = "",
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val author: GiteaUser = GiteaUser(),
|
||||
@SerialName("created_at") val createdAt: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GiteaSender(
|
||||
val id: Long = 0,
|
||||
val login: String = "",
|
||||
@SerialName("login_name") val loginName: String = "",
|
||||
@SerialName("full_name") val fullName: String = "",
|
||||
val email: String = "",
|
||||
@SerialName("avatar_url") val avatarUrl: String = "",
|
||||
)
|
||||
|
||||
// --- Event payloads ---
|
||||
|
||||
@Serializable
|
||||
data class PushPayload(
|
||||
val ref: String = "",
|
||||
val before: String = "",
|
||||
val after: String = "",
|
||||
@SerialName("compare_url") val compareUrl: String = "",
|
||||
val commits: List<GiteaCommit> = emptyList(),
|
||||
@SerialName("head_commit") val headCommit: GiteaCommit? = null,
|
||||
val pusher: GiteaUser? = null,
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
@SerialName("total_commits") val totalCommits: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class IssuesPayload(
|
||||
val action: String = "",
|
||||
val issue: GiteaIssue = GiteaIssue(),
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PullRequestPayload(
|
||||
val action: String = "",
|
||||
val number: Long = 0,
|
||||
@SerialName("pull_request") val pullRequest: GiteaPullRequest = GiteaPullRequest(),
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreatePayload(
|
||||
val ref: String = "",
|
||||
@SerialName("ref_type") val refType: String = "",
|
||||
val sha: String = "",
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeletePayload(
|
||||
val ref: String = "",
|
||||
@SerialName("ref_type") val refType: String = "",
|
||||
val sha: String = "",
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReleasePayload(
|
||||
val action: String = "",
|
||||
val release: GiteaRelease = GiteaRelease(),
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RepositoryPayload(
|
||||
val action: String = "",
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ForkPayload(
|
||||
@SerialName("forkee") val forkedRepo: GiteaRepository = GiteaRepository(),
|
||||
val repository: GiteaRepository = GiteaRepository(),
|
||||
val sender: GiteaUser = GiteaUser(),
|
||||
)
|
||||
|
||||
// --- Event type enum for routing ---
|
||||
|
||||
enum class GiteaEventType(val headerValue: String) {
|
||||
PUSH("push"),
|
||||
ISSUES("issues"),
|
||||
PULL_REQUEST("pull_request"),
|
||||
CREATE("create"),
|
||||
DELETE("delete"),
|
||||
RELEASE("release"),
|
||||
REPOSITORY("repository"),
|
||||
FORK("fork");
|
||||
|
||||
companion object {
|
||||
fun fromHeader(value: String?): GiteaEventType? =
|
||||
entries.find { it.headerValue.equals(value, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
package top.r3944realms.ltdmanager.module.gitea
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.module.BaseModule
|
||||
import top.r3944realms.ltdmanager.module.Modules
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.Executors
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class GiteaWebhookModule(
|
||||
moduleName: String,
|
||||
private val port: Int,
|
||||
private val webhookPath: String,
|
||||
private val secret: String,
|
||||
private val targetGroupId: Long,
|
||||
private val enabledEvents: Set<GiteaEventType>,
|
||||
) : BaseModule(Modules.GITEA_WEBHOOK, moduleName) {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
override fun onLoad() {
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope?.launch {
|
||||
try {
|
||||
startServer()
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 启动Webhook服务器失败", e)
|
||||
}
|
||||
}
|
||||
LoggerUtil.logger.info("[$name] Gitea Webhook模块已加载 (端口: $port, 路径: $webhookPath)")
|
||||
}
|
||||
|
||||
override suspend fun onUnload() {
|
||||
server?.stop(0)
|
||||
server = null
|
||||
scope?.cancel()
|
||||
LoggerUtil.logger.info("[$name] Gitea Webhook模块已卸载")
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
var executor = Executors.newFixedThreadPool(2)
|
||||
server = HttpServer.create(InetSocketAddress(port), 0).apply {
|
||||
createContext(webhookPath) { exchange ->
|
||||
scope?.launch { handleRequest(exchange) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
LoggerUtil.logger.info("[$name] Webhook服务器已启动在端口 $port$webhookPath")
|
||||
}
|
||||
|
||||
private suspend fun handleRequest(exchange: HttpExchange) {
|
||||
try {
|
||||
if (exchange.requestMethod != "POST") {
|
||||
exchange.sendResponseHeaders(405, -1)
|
||||
exchange.close()
|
||||
return
|
||||
}
|
||||
|
||||
val body = exchange.requestBody.bufferedReader().readText()
|
||||
val signatureHeader = exchange.requestHeaders.getFirst("X-Gitea-Signature") ?: ""
|
||||
val eventTypeHeader = exchange.requestHeaders.getFirst("X-Gitea-Event") ?: ""
|
||||
|
||||
// 验证签名
|
||||
if (!verifySignature(body, signatureHeader)) {
|
||||
LoggerUtil.logger.warn("[$name] 签名验证失败")
|
||||
exchange.sendResponseHeaders(403, -1)
|
||||
exchange.close()
|
||||
return
|
||||
}
|
||||
|
||||
val eventType = GiteaEventType.fromHeader(eventTypeHeader)
|
||||
if (eventType == null) {
|
||||
LoggerUtil.logger.warn("[$name] 未知事件类型: $eventTypeHeader")
|
||||
exchange.sendResponseHeaders(200, -1)
|
||||
exchange.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (eventType !in enabledEvents) {
|
||||
LoggerUtil.logger.debug("[$name] 跳过未启用的事件: $eventType")
|
||||
exchange.sendResponseHeaders(200, -1)
|
||||
exchange.close()
|
||||
return
|
||||
}
|
||||
|
||||
val message = formatEvent(eventType, body)
|
||||
if (message != null) {
|
||||
napCatClient.sendUnit(
|
||||
SendGroupMsgRequest(
|
||||
listOf(MessageElement.text(message)),
|
||||
ID.long(targetGroupId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
exchange.sendResponseHeaders(200, -1)
|
||||
exchange.close()
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 处理Webhook请求失败", e)
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, -1)
|
||||
exchange.close()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifySignature(payload: String, signatureHeader: String): Boolean {
|
||||
if (secret.isEmpty()) return true
|
||||
val expectedPrefix = "sha256="
|
||||
if (!signatureHeader.startsWith(expectedPrefix)) return false
|
||||
val receivedHex = signatureHeader.removePrefix(expectedPrefix)
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256"))
|
||||
val computedHex = mac.doFinal(payload.toByteArray(Charsets.UTF_8))
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
return computedHex.equals(receivedHex, ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun formatEvent(eventType: GiteaEventType, body: String): String? {
|
||||
return try {
|
||||
when (eventType) {
|
||||
GiteaEventType.PUSH -> formatPush(json.decodeFromString<PushPayload>(body))
|
||||
GiteaEventType.ISSUES -> formatIssues(json.decodeFromString<IssuesPayload>(body))
|
||||
GiteaEventType.PULL_REQUEST -> formatPullRequest(json.decodeFromString<PullRequestPayload>(body))
|
||||
GiteaEventType.CREATE -> formatCreate(json.decodeFromString<CreatePayload>(body))
|
||||
GiteaEventType.DELETE -> formatDelete(json.decodeFromString<DeletePayload>(body))
|
||||
GiteaEventType.RELEASE -> formatRelease(json.decodeFromString<ReleasePayload>(body))
|
||||
GiteaEventType.REPOSITORY -> formatRepository(json.decodeFromString<RepositoryPayload>(body))
|
||||
GiteaEventType.FORK -> formatFork(json.decodeFromString<ForkPayload>(body))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 解析Webhook事件失败 (type=$eventType)", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPush(p: PushPayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val branch = p.ref.removePrefix("refs/heads/").removePrefix("refs/tags/")
|
||||
val pusher = p.pusher?.login?.ifEmpty { p.pusher?.fullName } ?: p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val count = p.totalCommits
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("🔨 [$repo] Push 事件")
|
||||
sb.appendLine("👤 $pusher 推送了 $count 个提交到 $branch")
|
||||
if (p.commits.isNotEmpty()) {
|
||||
sb.appendLine("————————————")
|
||||
for (commit in p.commits.take(5)) {
|
||||
val shortSha = commit.id.take(7)
|
||||
val msg = commit.message.lines().first().take(50)
|
||||
sb.appendLine("• $shortSha $msg")
|
||||
}
|
||||
if (p.commits.size > 5) {
|
||||
sb.appendLine("• ... 还有 ${p.commits.size - 5} 个提交")
|
||||
}
|
||||
}
|
||||
if (p.compareUrl.isNotEmpty()) {
|
||||
sb.appendLine("————————————")
|
||||
sb.append("查看: ${p.compareUrl}")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun formatIssues(p: IssuesPayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val i = p.issue
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val action = translateAction(p.action)
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("📋 [$repo] Issue $action")
|
||||
sb.appendLine("👤 $user $action Issue #${i.number}: ${i.title}")
|
||||
if (i.body.isNotEmpty()) {
|
||||
sb.appendLine("————————————")
|
||||
sb.append(i.body.take(100).replace("\n", " "))
|
||||
}
|
||||
sb.appendLine()
|
||||
sb.append("查看: ${i.htmlUrl}")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun formatPullRequest(p: PullRequestPayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val pr = p.pullRequest
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val action = translateAction(p.action)
|
||||
val head = pr.headBranch.label.ifEmpty { pr.headBranch.ref }
|
||||
val base = pr.baseBranch.label.ifEmpty { pr.baseBranch.ref }
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("🔀 [$repo] PR $action")
|
||||
sb.appendLine("👤 $user $action PR #${pr.number}: ${pr.title}")
|
||||
sb.appendLine("$head → $base")
|
||||
if (pr.body.isNotEmpty()) {
|
||||
sb.appendLine("————————————")
|
||||
sb.appendLine(pr.body.take(150).replace("\n", " "))
|
||||
}
|
||||
sb.appendLine("————————————")
|
||||
val stateStr = when {
|
||||
pr.merged -> "已合并"
|
||||
pr.state == "closed" -> "已关闭"
|
||||
else -> "进行中"
|
||||
}
|
||||
sb.append("状态: $stateStr | 查看: ${pr.htmlUrl}")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun formatCreate(p: CreatePayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val typeName = when (p.refType) {
|
||||
"branch" -> "分支"
|
||||
"tag" -> "标签"
|
||||
else -> p.refType
|
||||
}
|
||||
return "➕ [$repo] 创建了$typeName: $p.ref\n👤 $user"
|
||||
}
|
||||
|
||||
private fun formatDelete(p: DeletePayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val typeName = when (p.refType) {
|
||||
"branch" -> "分支"
|
||||
"tag" -> "标签"
|
||||
else -> p.refType
|
||||
}
|
||||
return "❌ [$repo] 删除了$typeName: $p.ref\n👤 $user"
|
||||
}
|
||||
|
||||
private fun formatRelease(p: ReleasePayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val r = p.release
|
||||
val author = r.author.login.ifEmpty { r.author.fullName }
|
||||
val action = translateAction(p.action)
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("📦 [$repo] Release $action")
|
||||
sb.appendLine("🏷 ${r.tagName} - ${r.name.ifEmpty { r.tagName }}")
|
||||
sb.appendLine("👤 $author")
|
||||
if (r.body.isNotEmpty()) {
|
||||
sb.appendLine("————————————")
|
||||
sb.appendLine(r.body.take(200).replace("\n", " "))
|
||||
}
|
||||
sb.appendLine("————————————")
|
||||
val flags = listOfNotNull(
|
||||
if (r.draft) "草稿" else null,
|
||||
if (r.prerelease) "预发布" else null
|
||||
)
|
||||
if (flags.isNotEmpty()) sb.appendLine("标记: ${flags.joinToString(", ")}")
|
||||
sb.append("查看: ${r.htmlUrl}")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun formatRepository(p: RepositoryPayload): String {
|
||||
val repo = p.repository.fullName
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
val action = translateAction(p.action)
|
||||
return "📁 [$repo] Repository $action\n👤 $user"
|
||||
}
|
||||
|
||||
private fun formatFork(p: ForkPayload): String {
|
||||
val source = p.repository.fullName
|
||||
val fork = p.forkedRepo.fullName
|
||||
val user = p.sender.login.ifEmpty { p.sender.fullName }
|
||||
return "🍴 [$source] Fork\n👤 $user Fork 了仓库 → $fork\n查看: ${p.forkedRepo.htmlUrl}"
|
||||
}
|
||||
|
||||
private fun translateAction(action: String): String = when (action.lowercase()) {
|
||||
"opened" -> "已创建"
|
||||
"closed" -> "已关闭"
|
||||
"reopened" -> "已重新打开"
|
||||
"edited" -> "已编辑"
|
||||
"published" -> "已发布"
|
||||
"updated" -> "已更新"
|
||||
"created" -> "已创建"
|
||||
"deleted" -> "已删除"
|
||||
"labeled" -> "已标记"
|
||||
"unlabeled" -> "已取消标记"
|
||||
"assigned" -> "已指派"
|
||||
"unassigned" -> "已取消指派"
|
||||
"milestoned" -> "已设里程碑"
|
||||
"synchronized" -> "已同步"
|
||||
"review_requested" -> "请求审查"
|
||||
"transferred" -> "已转移"
|
||||
"merged" -> "已合并"
|
||||
else -> action
|
||||
}
|
||||
|
||||
override fun info(): String = "Gitea Webhook模块 - 监听端口$port, 接收${enabledEvents.size}种事件类型"
|
||||
override fun help(): String = """
|
||||
Gitea Webhook模块 - 接收Gitea Webhook事件并推送到QQ群
|
||||
监听地址: http://<host>:$port$webhookPath
|
||||
启用事件: ${enabledEvents.joinToString(", ") { it.headerValue }}
|
||||
""".trimIndent()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user