Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
6.8 kB · 212 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213package ui
import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "log/slog" "net/url" "time"
"provenance/internal/appview" "provenance/internal/atproto" "provenance/internal/domain" "provenance/internal/reputation" "provenance/internal/secure" "provenance/internal/storage" "provenance/internal/trust")
const browserKind = "browser-session"
type Auth interface { Metadata() any Begin(context.Context, string, string) (string, error) Finish(context.Context, url.Values, string) (atproto.Session, error) PutVouch(context.Context, atproto.Session, domain.Vouch) error DeleteVouch(context.Context, atproto.Session, string, string) error Logout(context.Context, atproto.Session) error}
type Accounts interface { Resolve(context.Context, string) (string, error) Search(context.Context, string) ([]atproto.Actor, error)}
type Appview interface { Join(context.Context, string) error Config(context.Context, string) (storage.Participant, error) SetThreshold(context.Context, string, float64) error Tags(context.Context, string) ([]string, error) Vouches(context.Context, string, int) ([]domain.Vouch, error) Evaluate(context.Context, string, string, string, domain.Mode, string, ...trust.Accounts) (appview.Evaluation, error) Aggregate(context.Context, string, string, string, domain.Mode) (reputation.Result, error)}
type Browser struct { Session atproto.Session `json:"session"` CSRF string `json:"csrf"`}
type Service struct { auth Auth accounts Accounts appview Appview vault *secure.Vault}
type Mutation struct { Saved bool `json:"saved"` Indexed bool `json:"indexed"`}
func New(auth Auth, accounts Accounts, appview Appview, vault *secure.Vault) *Service { return &Service{auth, accounts, appview, vault}}
func (s *Service) Metadata() any { return s.auth.Metadata() }func (s *Service) Begin(ctx context.Context, identifier, nonce string) (string, error) { identifier, err := accountInput(identifier) if err != nil { return "", err } return s.auth.Begin(ctx, identifier, nonce)}
func browserKey(token string) string { digest := sha256.Sum256([]byte(token)) return hex.EncodeToString(digest[:])}
func (s *Service) Finish(ctx context.Context, params url.Values, nonce string) (string, error) { session, err := s.auth.Finish(ctx, params, nonce) if err != nil { return "", err } if err := s.appview.Join(ctx, session.DID); err != nil { s.auth.Logout(ctx, session) return "", err } token := secure.Token() data, err := json.Marshal(Browser{session, secure.Token()}) if err != nil { return "", err } if err := s.vault.Add(ctx, browserKind, browserKey(token), data, time.Now().Add(atproto.SessionTTL)); err != nil { s.auth.Logout(ctx, session) return "", err } return token, nil}
func (s *Service) Session(ctx context.Context, token string) (Browser, error) { if token == "" { return Browser{}, errors.New("session is missing") } data, err := s.vault.Get(ctx, browserKind, browserKey(token)) if err != nil { return Browser{}, err } var browser Browser err = json.Unmarshal(data, &browser) return browser, err}
func (s *Service) Logout(ctx context.Context, token string, b Browser) error { if err := s.vault.Remove(ctx, browserKind, browserKey(token)); err != nil { return err } if err := s.auth.Logout(ctx, b.Session); err != nil { slog.Warn("PDS token revocation failed", "error", err) } return nil}
func (s *Service) Config(ctx context.Context, b Browser) (storage.Participant, error) { return s.appview.Config(ctx, b.Session.DID)}func (s *Service) Search(ctx context.Context, query string) ([]atproto.Actor, error) { return s.accounts.Search(ctx, query)}func (s *Service) Tags(ctx context.Context, prefix string) ([]string, error) { return s.appview.Tags(ctx, prefix)}func (s *Service) Vouches(ctx context.Context, b Browser, offset int) ([]domain.Vouch, error) { return s.appview.Vouches(ctx, b.Session.DID, offset)}
func (s *Service) SetThreshold(ctx context.Context, b Browser, value float64) error { if err := domain.ValidateThreshold(value); err != nil { return invalid("Choose a minimum trust score between -2 and 2.", err) } return s.appview.SetThreshold(ctx, b.Session.DID, value)}
func (s *Service) Evaluate(ctx context.Context, b Browser, target, tag string, mode domain.Mode, path string, options ...trust.Accounts) (appview.Evaluation, error) { if err := domain.ValidateQuery(tag, mode); err != nil { return appview.Evaluation{}, invalid("Choose a topic for topic-specific reputation. Topics contain at most 128 characters.", err) } target, err := accountInput(target) if err != nil { return appview.Evaluation{}, err } // The appview resolves all reputation identities together, including algorithm account lists. return s.appview.Evaluate(ctx, b.Session.DID, target, tag, mode, path, options...)}
func (s *Service) Put(ctx context.Context, b Browser, v domain.Vouch) (Mutation, error) { if err := domain.ValidateComment(v.Comment); err != nil { return Mutation{}, invalid("The comment must contain at most 300 characters.", err) } if err := domain.ValidateScore(v.Score); err != nil { return Mutation{}, invalid("Choose a trust level or the separate block option.", err) } if err := domain.ValidateTag(v.Tag); err != nil { return Mutation{}, invalid("The topic must contain at most 128 characters.", err) } account, err := accountInput(v.Subject) if err != nil { return Mutation{}, err } v.Subject = account did, err := s.accounts.Resolve(ctx, v.Subject) if err != nil { return Mutation{}, err } v.Subject = did if err := s.auth.PutVouch(ctx, b.Session, v); err != nil { return Mutation{}, err } return Mutation{Saved: true, Indexed: false}, nil}
func (s *Service) Aggregate(ctx context.Context, b Browser, target, tag string, mode domain.Mode) (reputation.Result, error) { if err := domain.ValidateQuery(tag, mode); err != nil { return reputation.Result{}, invalid("Choose a topic for topic-specific reputation. Topics contain at most 128 characters.", err) } target, err := accountInput(target) if err != nil { return reputation.Result{}, err } return s.appview.Aggregate(ctx, b.Session.DID, target, tag, mode)}
func (s *Service) Delete(ctx context.Context, b Browser, subject, tag string) (Mutation, error) { if err := domain.ValidateTag(tag); err != nil { return Mutation{}, invalid("The topic must contain at most 128 characters.", err) } subject, err := accountInput(subject) if err != nil { return Mutation{}, err } did, err := s.accounts.Resolve(ctx, subject) if err != nil { return Mutation{}, err } if err := s.auth.DeleteVouch(ctx, b.Session, did, tag); err != nil { return Mutation{}, err } return Mutation{Saved: true, Indexed: false}, nil}