package cache import ( "context" "fmt" "strings" "github.com/rs/zerolog" "github.com/vincentc-afk/gitea-notification-hub/internal/event" "github.com/vincentc-afk/gitea-notification-hub/internal/identity" "github.com/vincentc-afk/gitea-notification-hub/internal/storage" ) // 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 } // NewCachedResolver creates a new cached identity resolver func NewCachedResolver( repo storage.Repository, emailLookup identity.EmailLookup, slackLookup identity.SlackLookup, logger zerolog.Logger, ) *CachedResolver { return &CachedResolver{ repo: repo, emailLookup: emailLookup, slackLookup: slackLookup, logger: logger.With().Str("component", "identity-resolver").Logger(), } } // Resolve returns the external identity for a Gitea user // It follows this strategy: // 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 // 4. Cache the result for future lookups func (r *CachedResolver) Resolve(ctx context.Context, user event.User) (*identity.ResolvedIdentity, error) { logger := r.logger.With(). Str("gitea_username", user.GiteaUsername). Str("webhook_email", user.Email). Logger() // Step 1: Try to find by Gitea username in cache if user.GiteaUsername != "" { dbUser, err := r.repo.GetUserByGiteaUsername(ctx, user.GiteaUsername) if err == nil && dbUser.SlackID != "" { logger.Debug(). Str("cached_email", dbUser.Email). Str("slack_id", dbUser.SlackID). Msg("found user in cache by username") return &identity.ResolvedIdentity{ Email: dbUser.Email, SlackID: dbUser.SlackID, SlackName: dbUser.SlackName, }, nil } if err != nil && !storage.IsNotFound(err) { logger.Error().Err(err).Msg("error querying user by username") } } // Step 2: Not in cache - need to lookup real email via Gitea API // We don't trust the webhook email because it might be a noreply address logger.Info().Msg("user not in cache, querying Gitea API for real email") if user.GiteaUsername == "" { return nil, fmt.Errorf("no username available to lookup user") } if r.emailLookup == nil { return nil, fmt.Errorf("no email lookup provider configured") } // Get real email from Gitea API email, err := r.emailLookup.LookupEmail(ctx, user.GiteaUsername) if err != nil { logger.Error().Err(err).Msg("failed to lookup email from Gitea API") return nil, fmt.Errorf("looking up email for %s: %w", user.GiteaUsername, err) } logger.Info().Str("real_email", email).Msg("got real email from Gitea API") // Step 3: Lookup Slack ID by real email slackID, slackName, err := r.slackLookup.LookupSlackIDByEmail(ctx, email) if err != nil { logger.Warn().Err(err).Str("email", email).Msg("failed to lookup Slack ID") return nil, fmt.Errorf("looking up Slack ID for %s: %w", email, err) } // Step 4: Cache the result dbUser := &storage.User{ GiteaUsername: user.GiteaUsername, GiteaID: user.GiteaID, Email: strings.ToLower(email), FullName: user.FullName, SlackID: slackID, SlackName: slackName, } if err := r.repo.UpsertUser(ctx, dbUser); err != nil { logger.Error().Err(err).Msg("failed to cache user identity") // Continue anyway, we have the identity } else { logger.Info(). Str("slack_id", slackID). Str("slack_name", slackName). Msg("cached user identity") } return &identity.ResolvedIdentity{ Email: email, SlackID: slackID, SlackName: slackName, }, nil }