Files
at-container-registry/pkg/appview/holdclient/tier_update.go
T

98 lines
2.9 KiB
Go

// Package holdclient provides client functions for the appview to call hold XRPC endpoints.
package holdclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth"
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
// UpdateCrewTierOnHold calls io.atcr.hold.updateCrewTier on a specific hold.
// It signs a short-lived JWT with the appview's P-256 key and sends the tier update request.
func UpdateCrewTierOnHold(ctx context.Context, holdDID, holdURL, userDID string, tierRank int, privateKey *atcrypto.PrivateKeyP256, appviewDID string) error {
// Sign appview service token
token, err := auth.CreateAppviewServiceToken(privateKey, appviewDID, holdDID, userDID)
if err != nil {
return fmt.Errorf("failed to create appview token: %w", err)
}
// Build request body
body, err := json.Marshal(map[string]any{
"userDid": userDID,
"tierRank": tierRank,
})
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
// Build URL
url := strings.TrimSuffix(holdURL, "/") + atproto.HoldUpdateCrewTier
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to call updateCrewTier on %s: %w", holdDID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("updateCrewTier on %s returned %d: %s", holdDID, resp.StatusCode, string(respBody))
}
return nil
}
// urlFromDIDWeb converts a did:web to its HTTPS URL.
func urlFromDIDWeb(did string) string {
if !strings.HasPrefix(did, "did:web:") {
return ""
}
host := strings.TrimPrefix(did, "did:web:")
host = strings.ReplaceAll(host, "%3A", ":")
return "https://" + host
}
// UpdateCrewTierOnAllHolds pushes a tier update to all managed holds.
// It resolves each hold DID to a URL and calls updateCrewTier.
// Errors are logged but do not cause the function to fail — best effort.
func UpdateCrewTierOnAllHolds(ctx context.Context, managedHolds []string, userDID string, tierRank int, privateKey *atcrypto.PrivateKeyP256, appviewDID string) {
for _, holdDID := range managedHolds {
holdURL := urlFromDIDWeb(holdDID)
if holdURL == "" {
slog.Warn("Could not resolve hold DID to URL, skipping", "holdDID", holdDID)
continue
}
if err := UpdateCrewTierOnHold(ctx, holdDID, holdURL, userDID, tierRank, privateKey, appviewDID); err != nil {
slog.Error("Failed to update crew tier on hold",
"holdDID", holdDID,
"userDID", userDID,
"tierRank", tierRank,
"error", err,
)
} else {
slog.Info("Updated crew tier on hold",
"holdDID", holdDID,
"userDID", userDID,
"tierRank", tierRank,
)
}
}
}