Compare commits
18 Commits
765fb04420
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6337feeae1 | |||
| c288c85b84 | |||
| 39f49fbba8 | |||
| aafb820299 | |||
| 718569eda7 | |||
| 72b50b88f2 | |||
| c8b2dbe9dd | |||
| d33b1abd7e | |||
| e7d6abb942 | |||
| a4af4f4f78 | |||
| 357cecca84 | |||
| b706bc2338 | |||
| 1ab139baa4 | |||
| 895b82096c | |||
| 673cefeb90 | |||
| 9e26698068 | |||
| 8f08345bb2 | |||
| 371b5f4304 |
+9
-2
@@ -67,8 +67,15 @@ func main() {
|
||||
slackClient := slacknotifier.New(&cfg.Notification.Slack, logger)
|
||||
logger.Info().Msg("Slack notifier initialized")
|
||||
|
||||
// Initialize cached identity resolver
|
||||
resolver := cache.NewCachedResolver(repo, emailLookup, slackClient, logger)
|
||||
// Initialize cached identity resolver with optional manual mappings
|
||||
manualMappings := cache.ParseManualMappings(os.Getenv("MANUAL_USER_MAPPINGS"))
|
||||
if len(manualMappings) > 0 {
|
||||
logger.Info().Int("count", len(manualMappings)).Msg("manual user mappings configured")
|
||||
for username := range manualMappings {
|
||||
logger.Debug().Str("username", username).Msg("manual mapping registered")
|
||||
}
|
||||
}
|
||||
resolver := cache.NewCachedResolver(repo, emailLookup, slackClient, manualMappings, logger)
|
||||
logger.Info().Msg("identity resolver initialized")
|
||||
|
||||
// Create processor adapter that implements webhook.EventHandler
|
||||
|
||||
@@ -3,7 +3,6 @@ set -e
|
||||
|
||||
# Generate config.yaml from environment variables
|
||||
mkdir -p /app/config
|
||||
|
||||
cat > /app/config/config.yaml << 'YAMLEOF'
|
||||
server:
|
||||
port: 8080
|
||||
@@ -38,23 +37,25 @@ rules:
|
||||
notify_reviewers: true
|
||||
YAMLEOF
|
||||
|
||||
# Seed manual user mappings from MANUAL_USER_MAPPINGS env var (JSON: {"gitea_username":"slack_id",...})
|
||||
# Seed manual user mappings from MANUAL_USER_MAPPINGS env var
|
||||
# Format: "GiteaUsername:SlackID,GiteaUser2:SlackID2" (same as Go's ParseManualMappings)
|
||||
# These bypass the Gitea API email lookup + Slack email lookup
|
||||
|
||||
if [ -n "$MANUAL_USER_MAPPINGS" ]; then
|
||||
# Create table and indexes using single-line calls to avoid sh issues with multi-line SQL
|
||||
sqlite3 /app/data/notifications.db "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, gitea_username TEXT UNIQUE, gitea_id INTEGER, email TEXT UNIQUE, full_name TEXT, slack_id TEXT, slack_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);" 2>/dev/null || true
|
||||
sqlite3 /app/data/notifications.db "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);" 2>/dev/null || true
|
||||
sqlite3 /app/data/notifications.db "CREATE INDEX IF NOT EXISTS idx_users_gitea_username ON users(gitea_username);" 2>/dev/null || true
|
||||
sqlite3 /app/data/notifications.db "CREATE INDEX IF NOT EXISTS idx_users_slack_id ON users(slack_id);" 2>/dev/null || true
|
||||
|
||||
echo "$MANUAL_USER_MAPPINGS" | jq -r 'to_entries[] | "\(.key) \(.value)"' 2>/dev/null | \
|
||||
while read -r gitea_user slack_id; do
|
||||
[ -z "$gitea_user" ] && continue
|
||||
echo "$MANUAL_USER_MAPPINGS" | tr ',' '\n' | while IFS=':' read -r name slack_id; do
|
||||
name=$(echo "$name" | xargs)
|
||||
slack_id=$(echo "$slack_id" | xargs)
|
||||
[ -z "$name" ] && continue
|
||||
[ -z "$slack_id" ] && continue
|
||||
sqlite3 /app/data/notifications.db \
|
||||
"INSERT OR REPLACE INTO users(gitea_username, slack_id, slack_name, updated_at) VALUES('$gitea_user','$slack_id','$gitea_user',datetime('now'));" \
|
||||
"INSERT OR REPLACE INTO users(gitea_username, slack_id, slack_name, updated_at) VALUES('$name','$slack_id','$name',datetime('now'));" \
|
||||
2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
exec ./gitea-notification-hub -config /app/config/config.yaml
|
||||
exec ./gitea-notification-hub -config /app/config/config.yaml -debug
|
||||
|
||||
+565
-1
File diff suppressed because one or more lines are too long
Vendored
+54
-8
@@ -12,14 +12,40 @@ import (
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/storage"
|
||||
)
|
||||
|
||||
// ParseManualMappings parses a MANUAL_USER_MAPPINGS env var string
|
||||
// Format: "GiteaUsername:SlackID,GiteaUser2:SlackID2"
|
||||
func ParseManualMappings(raw string) map[string]string {
|
||||
mappings := make(map[string]string)
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return mappings
|
||||
}
|
||||
for _, pair := range strings.Split(raw, ",") {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(pair, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
username := strings.TrimSpace(parts[0])
|
||||
slackID := strings.TrimSpace(parts[1])
|
||||
if username != "" && slackID != "" {
|
||||
mappings[username] = slackID
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappings
|
||||
}
|
||||
|
||||
// CachedResolver implements identity.Resolver with caching
|
||||
// It stores resolved identities in the database and only queries
|
||||
// external APIs when a user is not found in the cache
|
||||
type CachedResolver struct {
|
||||
repo storage.Repository
|
||||
emailLookup identity.EmailLookup
|
||||
slackLookup identity.SlackLookup
|
||||
logger zerolog.Logger
|
||||
repo storage.Repository
|
||||
emailLookup identity.EmailLookup
|
||||
slackLookup identity.SlackLookup
|
||||
manualMappings map[string]string // GiteaUsername -> SlackID
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewCachedResolver creates a new cached identity resolver
|
||||
@@ -27,18 +53,24 @@ func NewCachedResolver(
|
||||
repo storage.Repository,
|
||||
emailLookup identity.EmailLookup,
|
||||
slackLookup identity.SlackLookup,
|
||||
manualMappings map[string]string,
|
||||
logger zerolog.Logger,
|
||||
) *CachedResolver {
|
||||
if manualMappings == nil {
|
||||
manualMappings = make(map[string]string)
|
||||
}
|
||||
return &CachedResolver{
|
||||
repo: repo,
|
||||
emailLookup: emailLookup,
|
||||
slackLookup: slackLookup,
|
||||
logger: logger.With().Str("component", "identity-resolver").Logger(),
|
||||
repo: repo,
|
||||
emailLookup: emailLookup,
|
||||
slackLookup: slackLookup,
|
||||
manualMappings: manualMappings,
|
||||
logger: logger.With().Str("component", "identity-resolver").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve returns the external identity for a Gitea user
|
||||
// It follows this strategy:
|
||||
// 0. Check manual mappings (MANUAL_USER_MAPPINGS env var) - highest priority
|
||||
// 1. Check DB by Gitea username - if found with Slack ID, return cached result
|
||||
// 2. If not found, use Gitea API to get the real email (not the webhook email which may be noreply)
|
||||
// 3. Lookup Slack by email
|
||||
@@ -49,6 +81,20 @@ func (r *CachedResolver) Resolve(ctx context.Context, user event.User) (*identit
|
||||
Str("webhook_email", user.Email).
|
||||
Logger()
|
||||
|
||||
// Step 0: Check manual mappings first (highest priority)
|
||||
if user.GiteaUsername != "" {
|
||||
if slackID, ok := r.manualMappings[user.GiteaUsername]; ok {
|
||||
logger.Info().
|
||||
Str("slack_id", slackID).
|
||||
Msg("resolved user via manual mapping")
|
||||
return &identity.ResolvedIdentity{
|
||||
Email: user.Email,
|
||||
SlackID: slackID,
|
||||
SlackName: "",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Try to find by Gitea username in cache
|
||||
if user.GiteaUsername != "" {
|
||||
dbUser, err := r.repo.GetUserByGiteaUsername(ctx, user.GiteaUsername)
|
||||
|
||||
+10
-10
@@ -82,7 +82,7 @@ type Review struct {
|
||||
|
||||
// Commit represents a Git commit in webhook payloads
|
||||
type Commit struct {
|
||||
ID string `json:"id"` // SHA
|
||||
ID string `json:"id"` // SHA
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Author GitUser `json:"author"`
|
||||
@@ -99,15 +99,15 @@ type GitUser struct {
|
||||
|
||||
// PullRequestEvent is the payload for pull_request webhooks
|
||||
type PullRequestEvent struct {
|
||||
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, review_requested, synchronize, etc.
|
||||
Number int64 `json:"number"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
|
||||
RequestedReviewer *GiteaUser `json:"requested_reviewer"` // Present on review_requested action (singular)
|
||||
Assignee *GiteaUser `json:"assignee"` // Present on assigned/unassigned action
|
||||
Commits []Commit `json:"commits"` // Present on synchronize action
|
||||
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, review_requested, synchronize, etc.
|
||||
Number int64 `json:"number"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
|
||||
RequestedReviewer *GiteaUser `json:"requested_reviewer"` // Present on review_requested action (singular)
|
||||
Assignee *GiteaUser `json:"assignee"` // Present on assigned/unassigned action
|
||||
Commits []Commit `json:"commits"` // Present on synchronize action
|
||||
}
|
||||
|
||||
// PullRequestReviewEvent is the payload for pull_request_review webhooks
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"$schema":"https://docs.renovatebot.com/renovate-schema.json","enabled":false}
|
||||
Reference in New Issue
Block a user