initial
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Subset of the Gitea webhook payload we actually consume. Gitea reuses the
|
||||
// same top-level structure across event types; fields are populated per event.
|
||||
type GiteaEvent struct {
|
||||
Action string `json:"action"`
|
||||
Sender User `json:"sender"`
|
||||
Repository Repository `json:"repository"`
|
||||
Issue *Issue `json:"issue,omitempty"`
|
||||
Comment *Comment `json:"comment,omitempty"`
|
||||
PullRequest *PullRequest `json:"pull_request,omitempty"`
|
||||
Review *Review `json:"review,omitempty"`
|
||||
RequestedReviewer *User `json:"requested_reviewer,omitempty"`
|
||||
Assignee *User `json:"assignee,omitempty"`
|
||||
Changes *Changes `json:"changes,omitempty"`
|
||||
}
|
||||
|
||||
// Changes is populated for action=="edited" events. We use it to detect when
|
||||
// an edit *introduced* an @Klaus mention versus an edit to a body that already
|
||||
// had one (which should not re-trigger us).
|
||||
type Changes struct {
|
||||
Body *ChangedString `json:"body,omitempty"`
|
||||
Title *ChangedString `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
type ChangedString struct {
|
||||
From string `json:"from"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Login string `json:"login"`
|
||||
Username string `json:"username"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (u User) Name() string {
|
||||
if u.Login != "" {
|
||||
return u.Login
|
||||
}
|
||||
return u.Username
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
FullName string `json:"full_name"`
|
||||
Name string `json:"name"`
|
||||
SSHURL string `json:"ssh_url"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
Owner User `json:"owner"`
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int64 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
User User `json:"user"`
|
||||
State string `json:"state"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
PullRequest *PullRefStub `json:"pull_request,omitempty"`
|
||||
Assignees []User `json:"assignees,omitempty"`
|
||||
}
|
||||
|
||||
// Gitea sets issue.pull_request to a small stub when an issue_comment fires on a PR.
|
||||
type PullRefStub struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int64 `json:"id"`
|
||||
Body string `json:"body"`
|
||||
User User `json:"user"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type PullRequest struct {
|
||||
Number int64 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
User User `json:"user"`
|
||||
State string `json:"state"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Head PRBranch `json:"head"`
|
||||
Base PRBranch `json:"base"`
|
||||
Assignees []User `json:"assignees,omitempty"`
|
||||
RequestedReviewers []User `json:"requested_reviewers,omitempty"`
|
||||
}
|
||||
|
||||
type PRBranch struct {
|
||||
Ref string `json:"ref"`
|
||||
SHA string `json:"sha"`
|
||||
}
|
||||
|
||||
type Review struct {
|
||||
Body string `json:"body"`
|
||||
User User `json:"user"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// Trigger describes why Klaus is being woken up. nil means: do nothing.
|
||||
type Trigger struct {
|
||||
Reason string // mention | assigned | review_requested | reply_on_klaus_pr
|
||||
Source string // issue | issue_comment | pull_request | pull_request_comment | pull_request_review
|
||||
Body string // text that triggered us
|
||||
Number int64 // issue or PR number
|
||||
IsPR bool
|
||||
BranchHint string // for PR-related triggers, the PR head ref to check out
|
||||
HTMLURL string
|
||||
}
|
||||
|
||||
var mentionRE = regexp.MustCompile(`(?i)(^|[^a-z0-9_])@klaus\b`)
|
||||
|
||||
func mentionsKlaus(s string) bool { return mentionRE.MatchString(s) }
|
||||
|
||||
func isKlaus(u User) bool {
|
||||
return strings.EqualFold(u.Login, "klaus") || strings.EqualFold(u.Username, "klaus")
|
||||
}
|
||||
|
||||
// shouldFireOnBody returns true when body changes warrant waking Klaus.
|
||||
// For action=="created" / "opened": fire when the body mentions Klaus.
|
||||
// For action=="edited": fire only when the edit *introduced* the mention
|
||||
// (mention in new body but not in changes.body.from) — prevents repeat
|
||||
// triggers when someone edits an unrelated part of a comment/issue/PR that
|
||||
// already mentioned Klaus.
|
||||
func shouldFireOnBody(action, body string, changes *Changes) bool {
|
||||
if !mentionsKlaus(body) {
|
||||
return false
|
||||
}
|
||||
if action == "edited" {
|
||||
if changes == nil || changes.Body == nil {
|
||||
// Body wasn't part of the edit (only title/labels/etc changed).
|
||||
return false
|
||||
}
|
||||
if mentionsKlaus(changes.Body.From) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DecideTrigger parses the raw payload and returns a Trigger only when Klaus
|
||||
// should respond. Returns (event, nil, nil) when the event is benign.
|
||||
func DecideTrigger(eventType string, raw []byte) (*GiteaEvent, *Trigger, error) {
|
||||
var ev GiteaEvent
|
||||
if err := json.Unmarshal(raw, &ev); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Never react to our own events — prevents feedback loops.
|
||||
if isKlaus(ev.Sender) {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
|
||||
switch eventType {
|
||||
case "issues":
|
||||
if ev.Issue == nil {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
if ev.Action == "assigned" && ev.Assignee != nil && isKlaus(*ev.Assignee) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "assigned", Source: "issue",
|
||||
Body: ev.Issue.Body, Number: ev.Issue.Number, HTMLURL: ev.Issue.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
if (ev.Action == "opened" || ev.Action == "edited") &&
|
||||
shouldFireOnBody(ev.Action, ev.Issue.Body, ev.Changes) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "mention", Source: "issue",
|
||||
Body: ev.Issue.Body, Number: ev.Issue.Number, HTMLURL: ev.Issue.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
case "issue_comment":
|
||||
if ev.Comment == nil || ev.Issue == nil {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
if ev.Action != "created" && ev.Action != "edited" {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
isPR := ev.Issue.PullRequest != nil
|
||||
src := "issue_comment"
|
||||
if isPR {
|
||||
src = "pull_request_comment"
|
||||
}
|
||||
if shouldFireOnBody(ev.Action, ev.Comment.Body, ev.Changes) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "mention", Source: src,
|
||||
Body: ev.Comment.Body, Number: ev.Issue.Number, IsPR: isPR, HTMLURL: ev.Comment.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
if ev.Action == "created" && isPR && isKlaus(ev.Issue.User) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "reply_on_klaus_pr", Source: src,
|
||||
Body: ev.Comment.Body, Number: ev.Issue.Number, IsPR: true, HTMLURL: ev.Comment.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
case "pull_request":
|
||||
if ev.PullRequest == nil {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
if ev.Action == "assigned" && ev.Assignee != nil && isKlaus(*ev.Assignee) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "assigned", Source: "pull_request",
|
||||
Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
|
||||
IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
if ev.Action == "review_requested" && ev.RequestedReviewer != nil && isKlaus(*ev.RequestedReviewer) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "review_requested", Source: "pull_request",
|
||||
Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
|
||||
IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
if (ev.Action == "opened" || ev.Action == "edited") &&
|
||||
shouldFireOnBody(ev.Action, ev.PullRequest.Body, ev.Changes) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "mention", Source: "pull_request",
|
||||
Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
|
||||
IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
case "pull_request_review":
|
||||
if ev.Review == nil || ev.PullRequest == nil || ev.Action != "submitted" {
|
||||
return &ev, nil, nil
|
||||
}
|
||||
if mentionsKlaus(ev.Review.Body) {
|
||||
return &ev, &Trigger{
|
||||
Reason: "mention", Source: "pull_request_review",
|
||||
Body: ev.Review.Body, Number: ev.PullRequest.Number,
|
||||
IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return &ev, nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user