From 673cefeb90ebf2826a37aff7498b72635f1d8602 Mon Sep 17 00:00:00 2001 From: JimKarvo Date: Mon, 29 Jun 2026 22:34:22 +0300 Subject: [PATCH] fix: add MANUAL_USER_MAPPINGS support for users with mismatched emails JimKarvo's Gitea email (dimitris@jksoftware.gr) differs from Slack email (jimkarvo@gmail.com), so the email-based Slack lookup fails. Add MANUAL_USER_MAPPINGS env var (format: GiteaUsername:SlackID,...) to bypass email lookup when a manual mapping exists. Changes: - cache.go: ParseManualMappings() parses env var format - cache.go: CachedResolver checks manual mappings first (step 0) - main.go: reads MANUAL_USER_MAPPINGS env var, passes to resolver --- cmd/server/main.go | 11 ++++-- internal/identity/cache/cache.go | 62 +++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index cd2be12..4a6a23a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 diff --git a/internal/identity/cache/cache.go b/internal/identity/cache/cache.go index e6e0924..c25716e 100644 --- a/internal/identity/cache/cache.go +++ b/internal/identity/cache/cache.go @@ -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)