Initial import with modified Dockerfile for env-based config generation
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// mentionRegex matches @username patterns
|
||||
// Supports alphanumeric characters, underscores, and hyphens
|
||||
var mentionRegex = regexp.MustCompile(`@([a-zA-Z0-9_-]+)`)
|
||||
|
||||
// ExtractMentions extracts all @mentioned usernames from text
|
||||
func ExtractMentions(text string) []string {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
matches := mentionRegex.FindAllStringSubmatch(text, -1)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deduplicate mentions
|
||||
seen := make(map[string]bool)
|
||||
var usernames []string
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
username := strings.ToLower(match[1])
|
||||
|
||||
// Skip common false positives
|
||||
if isCommonFalsePositive(username) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !seen[username] {
|
||||
seen[username] = true
|
||||
usernames = append(usernames, username)
|
||||
}
|
||||
}
|
||||
|
||||
return usernames
|
||||
}
|
||||
|
||||
// ReplaceMentionsWithSlackIDs replaces @username with <@SLACK_ID> format
|
||||
func ReplaceMentionsWithSlackIDs(text string, usernameToSlackID map[string]string) string {
|
||||
if text == "" || len(usernameToSlackID) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
result := mentionRegex.ReplaceAllStringFunc(text, func(match string) string {
|
||||
username := strings.ToLower(strings.TrimPrefix(match, "@"))
|
||||
if slackID, ok := usernameToSlackID[username]; ok {
|
||||
return "<@" + slackID + ">"
|
||||
}
|
||||
return match // Keep original if no mapping found
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isCommonFalsePositive checks if a mention is likely not a real username
|
||||
func isCommonFalsePositive(username string) bool {
|
||||
// Common patterns that look like mentions but aren't
|
||||
falsePositives := map[string]bool{
|
||||
"param": true,
|
||||
"returns": true,
|
||||
"throws": true,
|
||||
"deprecated": true,
|
||||
"see": true,
|
||||
"link": true,
|
||||
"code": true,
|
||||
"example": true,
|
||||
"todo": true,
|
||||
"fixme": true,
|
||||
"note": true,
|
||||
"warning": true,
|
||||
"author": true,
|
||||
"version": true,
|
||||
"since": true,
|
||||
}
|
||||
|
||||
return falsePositives[username]
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/config"
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/webhook"
|
||||
)
|
||||
|
||||
// IdentityResolver resolves Gitea users to external identities (e.g., Slack)
|
||||
type IdentityResolver interface {
|
||||
Resolve(ctx context.Context, user User) (*ResolvedIdentity, error)
|
||||
}
|
||||
|
||||
// ResolvedIdentity represents a resolved external identity
|
||||
type ResolvedIdentity struct {
|
||||
Email string `json:"email"`
|
||||
SlackID string `json:"slack_id"`
|
||||
SlackName string `json:"slack_name"`
|
||||
}
|
||||
|
||||
// Notifier sends notifications
|
||||
type Notifier interface {
|
||||
SendDirect(ctx context.Context, userID string, msg *Notification) error
|
||||
SendChannel(ctx context.Context, channel string, msg *Notification) error
|
||||
}
|
||||
|
||||
// Processor processes webhook events and generates notifications
|
||||
type Processor struct {
|
||||
cfg *config.Config
|
||||
resolver IdentityResolver
|
||||
notifier Notifier
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewProcessor creates a new event processor
|
||||
func NewProcessor(cfg *config.Config, resolver IdentityResolver, notifier Notifier, logger zerolog.Logger) *Processor {
|
||||
return &Processor{
|
||||
cfg: cfg,
|
||||
resolver: resolver,
|
||||
notifier: notifier,
|
||||
logger: logger.With().Str("component", "processor").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandlePullRequest processes pull request events
|
||||
func (p *Processor) HandlePullRequest(e *webhook.PullRequestEvent) {
|
||||
ctx := context.Background()
|
||||
event := p.normalizePREvent(e)
|
||||
|
||||
p.logger.Debug().
|
||||
Str("action", e.Action).
|
||||
Int("assignees_count", len(event.Assignees)).
|
||||
Int("reviewers_count", len(event.Reviewers)).
|
||||
Str("actor", event.Actor.GiteaUsername).
|
||||
Msg("processing PR event")
|
||||
|
||||
var usersToNotify []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
|
||||
switch e.Action {
|
||||
case "opened":
|
||||
event.Type = TypePROpened
|
||||
// Notify assignees and reviewers
|
||||
if p.cfg.Rules.PR.NotifyAssignees {
|
||||
for _, u := range event.Assignees {
|
||||
p.logger.Debug().Str("assignee", u.GiteaUsername).Msg("adding assignee to notify")
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonAssignee})
|
||||
}
|
||||
}
|
||||
if p.cfg.Rules.PR.NotifyReviewers {
|
||||
for _, u := range event.Reviewers {
|
||||
p.logger.Debug().Str("reviewer", u.GiteaUsername).Msg("adding reviewer to notify")
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonReviewer})
|
||||
}
|
||||
}
|
||||
|
||||
case "closed":
|
||||
if e.PullRequest.Merged {
|
||||
event.Type = TypePRMerged
|
||||
} else {
|
||||
event.Type = TypePRClosed
|
||||
}
|
||||
// Notify owner
|
||||
if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
|
||||
case "assigned":
|
||||
event.Type = TypePRAssigned
|
||||
// Notify newly assigned user
|
||||
if e.Assignee != nil && p.cfg.Rules.PR.NotifyAssignees {
|
||||
p.logger.Debug().Str("assignee", e.Assignee.Login).Msg("adding newly assigned user to notify")
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{giteaUserToUser(*e.Assignee), ReasonAssignee})
|
||||
}
|
||||
|
||||
case "review_requested":
|
||||
event.Type = TypePRReviewRequested
|
||||
// Notify newly requested reviewer (singular - Gitea sends one at a time)
|
||||
if e.RequestedReviewer != nil {
|
||||
p.logger.Debug().Str("requested_reviewer", e.RequestedReviewer.Login).Msg("review_requested event")
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{giteaUserToUser(*e.RequestedReviewer), ReasonReviewer})
|
||||
} else {
|
||||
p.logger.Debug().Msg("review_requested event but no RequestedReviewer field")
|
||||
}
|
||||
|
||||
case "synchronize":
|
||||
event.Type = TypePRSynchronized
|
||||
// Add commits info to event
|
||||
event.Commits = make([]CommitInfo, 0, len(e.Commits))
|
||||
for _, c := range e.Commits {
|
||||
// Get first line of commit message
|
||||
msg := c.Message
|
||||
if idx := strings.Index(msg, "\n"); idx > 0 {
|
||||
msg = msg[:idx]
|
||||
}
|
||||
event.Commits = append(event.Commits, CommitInfo{
|
||||
SHA: c.ID[:7], // Short SHA
|
||||
Message: msg,
|
||||
URL: c.URL,
|
||||
Author: c.Author.Name,
|
||||
})
|
||||
}
|
||||
// Notify owner (will be filtered out if they pushed the commits)
|
||||
if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
// Notify reviewers
|
||||
if p.cfg.Rules.PR.NotifyReviewers {
|
||||
for _, u := range event.Reviewers {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonReviewer})
|
||||
}
|
||||
}
|
||||
// Notify assignees
|
||||
if p.cfg.Rules.PR.NotifyAssignees {
|
||||
for _, u := range event.Assignees {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonAssignee})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the actor from notifications (don't notify yourself)
|
||||
beforeFilter := len(usersToNotify)
|
||||
usersToNotify = p.filterOutActor(usersToNotify, event.Actor)
|
||||
p.logger.Debug().
|
||||
Int("before_filter", beforeFilter).
|
||||
Int("after_filter", len(usersToNotify)).
|
||||
Str("actor_filtered", event.Actor.GiteaUsername).
|
||||
Msg("filtered out actor from notifications")
|
||||
|
||||
// Send notifications
|
||||
p.sendNotifications(ctx, event, usersToNotify)
|
||||
}
|
||||
|
||||
// HandlePullRequestReview processes PR review events
|
||||
func (p *Processor) HandlePullRequestReview(e *webhook.PullRequestReviewEvent) {
|
||||
ctx := context.Background()
|
||||
event := p.normalizeReviewEvent(e)
|
||||
|
||||
var usersToNotify []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
|
||||
// Notify PR owner about the review
|
||||
if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
|
||||
// Remove the actor
|
||||
usersToNotify = p.filterOutActor(usersToNotify, event.Actor)
|
||||
|
||||
p.sendNotifications(ctx, event, usersToNotify)
|
||||
}
|
||||
|
||||
// HandlePullRequestComment processes PR comment events
|
||||
func (p *Processor) HandlePullRequestComment(e *webhook.PullRequestCommentEvent) {
|
||||
ctx := context.Background()
|
||||
event := p.normalizePRCommentEvent(e)
|
||||
|
||||
if e.Action != "created" {
|
||||
return // Only notify on new comments
|
||||
}
|
||||
|
||||
var usersToNotify []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
|
||||
// Notify PR owner
|
||||
if p.cfg.Rules.Comment.NotifyThreadOwner && event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
|
||||
// Extract and notify mentioned users
|
||||
if p.cfg.Rules.Comment.NotifyMentioned {
|
||||
mentionedUsers := ExtractMentions(e.Comment.Body)
|
||||
for _, username := range mentionedUsers {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{User{GiteaUsername: username}, ReasonMention})
|
||||
}
|
||||
}
|
||||
|
||||
// Notify reviewers
|
||||
if p.cfg.Rules.Comment.NotifyReviewers {
|
||||
for _, u := range event.Reviewers {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonReviewer})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the actor
|
||||
usersToNotify = p.filterOutActor(usersToNotify, event.Actor)
|
||||
|
||||
p.sendNotifications(ctx, event, usersToNotify)
|
||||
}
|
||||
|
||||
// HandleIssue processes issue events
|
||||
func (p *Processor) HandleIssue(e *webhook.IssueEvent) {
|
||||
ctx := context.Background()
|
||||
event := p.normalizeIssueEvent(e)
|
||||
|
||||
var usersToNotify []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
|
||||
switch e.Action {
|
||||
case "opened":
|
||||
event.Type = TypeIssueOpened
|
||||
// Notify assignees
|
||||
if p.cfg.Rules.Issue.NotifyAssignees {
|
||||
for _, u := range event.Assignees {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{u, ReasonAssignee})
|
||||
}
|
||||
}
|
||||
|
||||
case "closed":
|
||||
event.Type = TypeIssueClosed
|
||||
// Notify owner
|
||||
if event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the actor
|
||||
usersToNotify = p.filterOutActor(usersToNotify, event.Actor)
|
||||
|
||||
p.sendNotifications(ctx, event, usersToNotify)
|
||||
}
|
||||
|
||||
// HandleIssueComment processes issue comment events
|
||||
func (p *Processor) HandleIssueComment(e *webhook.IssueCommentEvent) {
|
||||
ctx := context.Background()
|
||||
event := p.normalizeIssueCommentEvent(e)
|
||||
|
||||
if e.Action != "created" {
|
||||
return
|
||||
}
|
||||
|
||||
var usersToNotify []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
|
||||
// Notify issue owner
|
||||
if p.cfg.Rules.Comment.NotifyThreadOwner && event.Owner != nil {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{*event.Owner, ReasonOwner})
|
||||
}
|
||||
|
||||
// Extract and notify mentioned users
|
||||
if p.cfg.Rules.Comment.NotifyMentioned {
|
||||
mentionedUsers := ExtractMentions(e.Comment.Body)
|
||||
for _, username := range mentionedUsers {
|
||||
usersToNotify = append(usersToNotify, struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}{User{GiteaUsername: username}, ReasonMention})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the actor
|
||||
usersToNotify = p.filterOutActor(usersToNotify, event.Actor)
|
||||
|
||||
p.sendNotifications(ctx, event, usersToNotify)
|
||||
}
|
||||
|
||||
// sendNotifications resolves users and sends notifications
|
||||
func (p *Processor) sendNotifications(ctx context.Context, event *Event, users []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}) {
|
||||
// Deduplicate users
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, u := range users {
|
||||
key := u.user.GiteaUsername
|
||||
if key == "" {
|
||||
key = u.user.Email
|
||||
}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
// Resolve user identity
|
||||
identity, err := p.resolver.Resolve(ctx, u.user)
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Err(err).
|
||||
Str("username", u.user.GiteaUsername).
|
||||
Msg("failed to resolve user identity")
|
||||
continue
|
||||
}
|
||||
|
||||
if identity.SlackID == "" {
|
||||
p.logger.Debug().
|
||||
Str("username", u.user.GiteaUsername).
|
||||
Msg("user has no Slack ID, skipping notification")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create notification
|
||||
notification := &Notification{
|
||||
TargetUser: u.user,
|
||||
Event: event,
|
||||
Reason: u.reason,
|
||||
Message: p.formatMessage(event, u.reason),
|
||||
}
|
||||
|
||||
// Send DM
|
||||
if err := p.notifier.SendDirect(ctx, identity.SlackID, notification); err != nil {
|
||||
p.logger.Error().
|
||||
Err(err).
|
||||
Str("slack_id", identity.SlackID).
|
||||
Msg("failed to send notification")
|
||||
} else {
|
||||
p.logger.Info().
|
||||
Str("slack_id", identity.SlackID).
|
||||
Str("reason", string(u.reason)).
|
||||
Str("event_type", string(event.Type)).
|
||||
Msg("notification sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filterOutActor removes the event actor from the notification list
|
||||
func (p *Processor) filterOutActor(users []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}, actor User) []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
} {
|
||||
var filtered []struct {
|
||||
user User
|
||||
reason NotificationReason
|
||||
}
|
||||
for _, u := range users {
|
||||
if u.user.GiteaUsername != actor.GiteaUsername {
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// formatMessage creates a human-readable notification message
|
||||
func (p *Processor) formatMessage(event *Event, reason NotificationReason) string {
|
||||
// This will be enhanced later with templates
|
||||
switch event.Type {
|
||||
case TypePROpened:
|
||||
return "New PR opened"
|
||||
case TypePRClosed:
|
||||
return "PR closed"
|
||||
case TypePRMerged:
|
||||
return "PR merged"
|
||||
case TypePRReviewRequested:
|
||||
return "Review requested"
|
||||
case TypePRReviewed:
|
||||
return "PR reviewed"
|
||||
case TypePRCommented:
|
||||
return "New comment on PR"
|
||||
case TypeIssueOpened:
|
||||
return "New issue opened"
|
||||
case TypeIssueClosed:
|
||||
return "Issue closed"
|
||||
case TypeIssueCommented:
|
||||
return "New comment on issue"
|
||||
default:
|
||||
return "New notification"
|
||||
}
|
||||
}
|
||||
|
||||
// Normalization helpers
|
||||
func (p *Processor) normalizePREvent(e *webhook.PullRequestEvent) *Event {
|
||||
owner := giteaUserToUser(e.PullRequest.User)
|
||||
|
||||
// Get reviewers from both possible locations:
|
||||
// - e.PullRequest.RequestedReviewers: present in opened action
|
||||
// - e.RequestedReviewers: present in review_requested action
|
||||
var reviewers []User
|
||||
if len(e.PullRequest.RequestedReviewers) > 0 {
|
||||
reviewers = giteaUsersToUsers(e.PullRequest.RequestedReviewers)
|
||||
} else if len(e.RequestedReviewers) > 0 {
|
||||
reviewers = giteaUsersToUsers(e.RequestedReviewers)
|
||||
}
|
||||
|
||||
return &Event{
|
||||
Timestamp: time.Now(),
|
||||
Actor: giteaUserToUser(e.Sender),
|
||||
RepoName: e.Repository.Name,
|
||||
RepoFullName: e.Repository.FullName,
|
||||
RepoURL: e.Repository.HTMLURL,
|
||||
Number: e.PullRequest.Number,
|
||||
Title: e.PullRequest.Title,
|
||||
Body: e.PullRequest.Body,
|
||||
URL: e.PullRequest.HTMLURL,
|
||||
Owner: &owner,
|
||||
Assignees: giteaUsersToUsers(e.PullRequest.Assignees),
|
||||
Reviewers: reviewers,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) normalizeReviewEvent(e *webhook.PullRequestReviewEvent) *Event {
|
||||
owner := giteaUserToUser(e.PullRequest.User)
|
||||
return &Event{
|
||||
Type: TypePRReviewed,
|
||||
Timestamp: time.Now(),
|
||||
Actor: giteaUserToUser(e.Sender),
|
||||
RepoName: e.Repository.Name,
|
||||
RepoFullName: e.Repository.FullName,
|
||||
RepoURL: e.Repository.HTMLURL,
|
||||
Number: e.PullRequest.Number,
|
||||
Title: e.PullRequest.Title,
|
||||
URL: e.PullRequest.HTMLURL,
|
||||
Owner: &owner,
|
||||
ReviewState: e.Review.State,
|
||||
CommentBody: e.Review.Body,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) normalizePRCommentEvent(e *webhook.PullRequestCommentEvent) *Event {
|
||||
owner := giteaUserToUser(e.PullRequest.User)
|
||||
return &Event{
|
||||
Type: TypePRCommented,
|
||||
Timestamp: time.Now(),
|
||||
Actor: giteaUserToUser(e.Sender),
|
||||
RepoName: e.Repository.Name,
|
||||
RepoFullName: e.Repository.FullName,
|
||||
RepoURL: e.Repository.HTMLURL,
|
||||
Number: e.PullRequest.Number,
|
||||
Title: e.PullRequest.Title,
|
||||
URL: e.PullRequest.HTMLURL,
|
||||
Owner: &owner,
|
||||
Reviewers: giteaUsersToUsers(e.PullRequest.RequestedReviewers),
|
||||
CommentBody: e.Comment.Body,
|
||||
CommentURL: e.Comment.HTMLURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) normalizeIssueEvent(e *webhook.IssueEvent) *Event {
|
||||
owner := giteaUserToUser(e.Issue.User)
|
||||
return &Event{
|
||||
Timestamp: time.Now(),
|
||||
Actor: giteaUserToUser(e.Sender),
|
||||
RepoName: e.Repository.Name,
|
||||
RepoFullName: e.Repository.FullName,
|
||||
RepoURL: e.Repository.HTMLURL,
|
||||
Number: e.Issue.Number,
|
||||
Title: e.Issue.Title,
|
||||
Body: e.Issue.Body,
|
||||
URL: e.Issue.HTMLURL,
|
||||
Owner: &owner,
|
||||
Assignees: giteaUsersToUsers(e.Issue.Assignees),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) normalizeIssueCommentEvent(e *webhook.IssueCommentEvent) *Event {
|
||||
owner := giteaUserToUser(e.Issue.User)
|
||||
return &Event{
|
||||
Type: TypeIssueCommented,
|
||||
Timestamp: time.Now(),
|
||||
Actor: giteaUserToUser(e.Sender),
|
||||
RepoName: e.Repository.Name,
|
||||
RepoFullName: e.Repository.FullName,
|
||||
RepoURL: e.Repository.HTMLURL,
|
||||
Number: e.Issue.Number,
|
||||
Title: e.Issue.Title,
|
||||
URL: e.Issue.HTMLURL,
|
||||
Owner: &owner,
|
||||
CommentBody: e.Comment.Body,
|
||||
CommentURL: e.Comment.HTMLURL,
|
||||
}
|
||||
}
|
||||
|
||||
func giteaUserToUser(u webhook.GiteaUser) User {
|
||||
username := u.Login
|
||||
if username == "" {
|
||||
username = u.Username
|
||||
}
|
||||
return User{
|
||||
GiteaID: u.ID,
|
||||
GiteaUsername: username,
|
||||
Email: u.Email,
|
||||
FullName: u.FullName,
|
||||
}
|
||||
}
|
||||
|
||||
func giteaUsersToUsers(users []webhook.GiteaUser) []User {
|
||||
result := make([]User, len(users))
|
||||
for i, u := range users {
|
||||
result[i] = giteaUserToUser(u)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package event
|
||||
|
||||
import "time"
|
||||
|
||||
// Type represents the normalized event type
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypePROpened Type = "pr_opened"
|
||||
TypePRClosed Type = "pr_closed"
|
||||
TypePRMerged Type = "pr_merged"
|
||||
TypePRAssigned Type = "pr_assigned"
|
||||
TypePRReviewRequested Type = "pr_review_requested"
|
||||
TypePRReviewed Type = "pr_reviewed"
|
||||
TypePRCommented Type = "pr_commented"
|
||||
TypePRSynchronized Type = "pr_synchronized" // New commits pushed
|
||||
TypeIssueOpened Type = "issue_opened"
|
||||
TypeIssueClosed Type = "issue_closed"
|
||||
TypeIssueCommented Type = "issue_commented"
|
||||
)
|
||||
|
||||
// User represents a user in our normalized event model
|
||||
type User struct {
|
||||
GiteaID int64 `json:"gitea_id"`
|
||||
GiteaUsername string `json:"gitea_username"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
|
||||
// Event is a normalized event from Gitea
|
||||
type Event struct {
|
||||
ID string `json:"id"` // Delivery ID from Gitea
|
||||
Type Type `json:"type"` // Normalized event type
|
||||
Timestamp time.Time `json:"timestamp"` // When the event occurred
|
||||
|
||||
// The user who triggered the event
|
||||
Actor User `json:"actor"`
|
||||
|
||||
// Repository info
|
||||
RepoName string `json:"repo_name"`
|
||||
RepoFullName string `json:"repo_full_name"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
|
||||
// PR/Issue info
|
||||
Number int64 `json:"number"` // PR or Issue number
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
URL string `json:"url"`
|
||||
|
||||
// Users to potentially notify (based on roles)
|
||||
Owner *User `json:"owner,omitempty"` // PR/Issue author
|
||||
Assignees []User `json:"assignees,omitempty"` // Assigned users
|
||||
Reviewers []User `json:"reviewers,omitempty"` // Requested reviewers (PR only)
|
||||
|
||||
// For review events
|
||||
ReviewState string `json:"review_state,omitempty"` // APPROVED, CHANGES_REQUESTED, COMMENT
|
||||
|
||||
// For comment events
|
||||
CommentBody string `json:"comment_body,omitempty"`
|
||||
CommentURL string `json:"comment_url,omitempty"`
|
||||
|
||||
// For synchronize events (new commits pushed)
|
||||
Commits []CommitInfo `json:"commits,omitempty"`
|
||||
}
|
||||
|
||||
// CommitInfo represents a commit in a synchronized event
|
||||
type CommitInfo struct {
|
||||
SHA string `json:"sha"`
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
// Notification represents a notification to be sent
|
||||
type Notification struct {
|
||||
// Target user to notify
|
||||
TargetUser User `json:"target_user"`
|
||||
|
||||
// The event that triggered this notification
|
||||
Event *Event `json:"event"`
|
||||
|
||||
// Why this user is being notified
|
||||
Reason NotificationReason `json:"reason"`
|
||||
|
||||
// Message content
|
||||
Message string `json:"message"`
|
||||
|
||||
// Channel override (empty = DM)
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationReason explains why a user is being notified
|
||||
type NotificationReason string
|
||||
|
||||
const (
|
||||
ReasonOwner NotificationReason = "owner" // PR/Issue author
|
||||
ReasonAssignee NotificationReason = "assignee" // Assigned to PR/Issue
|
||||
ReasonReviewer NotificationReason = "reviewer" // Requested reviewer
|
||||
ReasonMention NotificationReason = "mention" // @mentioned in comment
|
||||
)
|
||||
Reference in New Issue
Block a user