Files
2025-11-01 11:08:53 -05:00

94 lines
3.0 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth/oauth"
"atcr.io/pkg/auth/token"
)
// EnsureCrewMembership attempts to register the user as a crew member on their default hold.
// The hold's requestCrew endpoint handles all authorization logic (checking allowAllCrew, existing membership, etc).
// This is best-effort and does not fail on errors.
func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, defaultHoldDID string) {
if defaultHoldDID == "" {
return
}
// Normalize URL to DID if needed
holdDID := atproto.ResolveHoldDIDFromURL(defaultHoldDID)
if holdDID == "" {
slog.Warn("failed to resolve hold DID", "defaultHold", defaultHoldDID)
return
}
// Resolve hold DID to HTTP endpoint
holdEndpoint := atproto.ResolveHoldURL(holdDID)
// Get service token for the hold
// Only works with OAuth (refresher required) - app passwords can't get service tokens
if refresher == nil {
slog.Debug("skipping crew registration - no OAuth refresher (app password flow)", "holdDID", holdDID)
return
}
// Wrap the refresher to match OAuthSessionRefresher interface
serviceToken, err := token.GetOrFetchServiceToken(ctx, refresher, client.DID(), holdDID, client.PDSEndpoint())
if err != nil {
slog.Warn("failed to get service token", "holdDID", holdDID, "error", err)
return
}
// Call requestCrew endpoint - it handles all the logic:
// - Checks allowAllCrew flag
// - Checks if already a crew member (returns success if so)
// - Creates crew record if authorized
if err := requestCrewMembership(ctx, holdEndpoint, serviceToken); err != nil {
slog.Warn("failed to request crew membership", "holdDID", holdDID, "error", err)
return
}
slog.Info("successfully registered as crew member", "holdDID", holdDID, "userDID", client.DID())
}
// requestCrewMembership calls the hold's requestCrew endpoint
// The endpoint handles all authorization and duplicate checking internally
func requestCrewMembership(ctx context.Context, holdEndpoint, serviceToken string) error {
// Add 5 second timeout to prevent hanging on offline holds
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
url := fmt.Sprintf("%s%s", holdEndpoint, atproto.HoldRequestCrew)
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+serviceToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// Read response body to capture actual error message from hold
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("requestCrew failed with status %d (failed to read error body: %w)", resp.StatusCode, readErr)
}
return fmt.Errorf("requestCrew failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}