125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
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
|
|
}
|