diff --git a/appview/db/profile.go b/appview/db/profile.go index a21ba6a7..957fd3f6 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -4,6 +4,9 @@ import ( "database/sql" "fmt" "log" + "net/url" + "slices" + "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" @@ -337,7 +340,7 @@ func UpsertProfile(tx *sql.Tx, profile *Profile) error { ) if err != nil { - log.Println("profile_pinned_repositories") + log.Println("profile_pinned_repositories", "err", err) return err } } @@ -447,3 +450,81 @@ func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) { return result, nil } + +func ValidateProfile(e Execer, profile *Profile) error { + // ensure description is not too long + if len(profile.Description) > 256 { + return fmt.Errorf("Entered bio is too long.") + } + + // ensure description is not too long + if len(profile.Location) > 40 { + return fmt.Errorf("Entered location is too long.") + } + + // ensure links are in order + err := validateLinks(profile) + if err != nil { + return err + } + + // ensure all pinned repos are either own repos or collaborating repos + repos, err := GetAllReposByDid(e, profile.Did) + if err != nil { + log.Printf("getting repos for %s: %s", profile.Did, err) + } + + collaboratingRepos, err := CollaboratingIn(e, profile.Did) + if err != nil { + log.Printf("getting collaborating repos for %s: %s", profile.Did, err) + } + + var validRepos []syntax.ATURI + for _, r := range repos { + validRepos = append(validRepos, r.RepoAt()) + } + for _, r := range collaboratingRepos { + validRepos = append(validRepos, r.RepoAt()) + } + + for _, pinned := range profile.PinnedRepos { + if pinned == "" { + continue + } + if !slices.Contains(validRepos, pinned) { + return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned) + } + } + + return nil +} + +func validateLinks(profile *Profile) error { + for i, link := range profile.Links { + if link == "" { + continue + } + + parsedURL, err := url.Parse(link) + if err != nil { + return fmt.Errorf("Invalid URL '%s': %v\n", link, err) + } + + if parsedURL.Scheme == "" { + if strings.HasPrefix(link, "//") { + profile.Links[i] = "https:" + link + } else { + profile.Links[i] = "https://" + link + } + continue + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme) + } + + // catch relative paths + if parsedURL.Host == "" { + return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link) + } + } + return nil +} diff --git a/appview/ingester.go b/appview/ingester.go index 8079eb3c..05fa708d 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -41,6 +41,8 @@ func Ingest(d db.DbWrapper) Ingester { ingestPublicKey(&d, e) case tangled.RepoArtifactNSID: ingestArtifact(&d, e) + case tangled.ActorProfileNSID: + ingestProfile(&d, e) } return err @@ -143,7 +145,6 @@ func ingestArtifact(d *db.DbWrapper, e *models.Event) error { switch e.Commit.Operation { case models.CommitOperationCreate, models.CommitOperationUpdate: - log.Println("processing add of artifact") raw := json.RawMessage(e.Commit.Record) record := tangled.RepoArtifact{} err = json.Unmarshal(raw, &record) @@ -176,7 +177,6 @@ func ingestArtifact(d *db.DbWrapper, e *models.Event) error { err = db.AddArtifact(d, artifact) case models.CommitOperationDelete: - log.Println("processing delete of artifact") err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) } @@ -186,3 +186,94 @@ func ingestArtifact(d *db.DbWrapper, e *models.Event) error { return nil } + +func ingestProfile(d *db.DbWrapper, e *models.Event) error { + did := e.Did + var err error + + if e.Commit.RKey != "self" { + return fmt.Errorf("ingestProfile only ingests `self` record") + } + + switch e.Commit.Operation { + case models.CommitOperationCreate, models.CommitOperationUpdate: + raw := json.RawMessage(e.Commit.Record) + record := tangled.ActorProfile{} + err = json.Unmarshal(raw, &record) + if err != nil { + log.Printf("invalid record: %s", err) + return err + } + + description := "" + if record.Description != nil { + description = *record.Description + } + + includeBluesky := false + if record.Bluesky != nil { + includeBluesky = *record.Bluesky + } + + location := "" + if record.Location != nil { + location = *record.Location + } + + var links [5]string + for i, l := range record.Links { + if i < 5 { + links[i] = l + } + } + + var stats [2]db.VanityStat + for i, s := range record.Stats { + if i < 2 { + stats[i].Kind = db.VanityStatKind(s) + } + } + + var pinned [6]syntax.ATURI + for i, r := range record.PinnedRepositories { + if i < 6 { + pinned[i] = syntax.ATURI(r) + } + } + + profile := db.Profile{ + Did: did, + Description: description, + IncludeBluesky: includeBluesky, + Location: location, + Links: links, + Stats: stats, + PinnedRepos: pinned, + } + + ddb, ok := d.Execer.(*db.DB) + if !ok { + return fmt.Errorf("failed to index profile record, invalid db cast") + } + + tx, err := ddb.Begin() + if err != nil { + return fmt.Errorf("failed to start transaction") + } + + err = db.ValidateProfile(tx, &profile) + if err != nil { + return fmt.Errorf("invalid profile record") + } + + err = db.UpsertProfile(tx, &profile) + case models.CommitOperationDelete: + err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) + } + + if err != nil { + return fmt.Errorf("failed to %s profile record: %w", e.Commit.Operation, err) + } + + return nil +} diff --git a/appview/pages/templates/user/profile.html b/appview/pages/templates/user/profile.html index f56bd4e8..3d80c00a 100644 --- a/appview/pages/templates/user/profile.html +++ b/appview/pages/templates/user/profile.html @@ -227,31 +227,35 @@ {{ define "profileCard" }}
{{ didOrHandle .UserDid .UserHandle }}
+ +{{ .Profile.Description }}
+ {{ $profile := .Profile }} + {{ with .Profile }} + + {{ if .Description }} +{{ .Description }}
{{ end }} -