forked from tangled.org/core
this repo has no description

appview: ingester: ingest profile records from the firehose

Changed files
+209 -102
appview
db
pages
templates
state
+82 -1
appview/db/profile.go
···
"database/sql"
"fmt"
"log"
+
"net/url"
+
"slices"
+
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
···
)
if err != nil {
-
log.Println("profile_pinned_repositories")
+
log.Println("profile_pinned_repositories", "err", err)
return err
}
}
···
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
+
}
+93 -2
appview/ingester.go
···
ingestPublicKey(&d, e)
case tangled.RepoArtifactNSID:
ingestArtifact(&d, e)
+
case tangled.ActorProfileNSID:
+
ingestProfile(&d, e)
}
return err
···
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)
···
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))
}
···
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
+
}
+26 -18
appview/pages/templates/user/profile.html
···
{{ define "profileCard" }}
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-3 items-center">
-
<div id="avatar" class="col-span-1 md-col-span-full flex justify-center items-center">
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
{{ if .AvatarUri }}
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
{{ end }}
</div>
-
<div class="col-span-2 md:col-span-full">
+
<div class="col-span-2">
<p title="{{ didOrHandle .UserDid .UserHandle }}"
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
{{ didOrHandle .UserDid .UserHandle }}
</p>
+
+
<div class="md:hidden">
+
{{ block "followerFollowing" .ProfileStats }} {{ end }}
+
</div>
+
</div>
+
<div class="col-span-3 md:col-span-full">
<div id="profile-bio" class="text-sm">
-
{{ if .Profile }}
-
<p>{{ .Profile.Description }}</p>
+
{{ $profile := .Profile }}
+
{{ with .Profile }}
+
+
{{ if .Description }}
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
{{ end }}
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
-
<span id="followers">{{ .ProfileStats.Followers }} followers</span>
-
<span class="select-none after:content-['·']"></span>
-
<span id="following">{{ .ProfileStats.Following }} following</span>
+
<div class="hidden md:block">
+
{{ block "followerFollowing" $.ProfileStats }} {{ end }}
</div>
-
{{ $profile := .Profile }}
-
{{ with .Profile }}
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
{{ if .Location }}
<div class="flex items-center gap-2">
···
<span>{{ .Location }}</span>
</div>
{{ end }}
-
{{ if .IncludeBluesky }}
<div class="flex items-center gap-2">
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
···
</a>
</div>
{{ end }}
-
{{ range $link := .Links }}
{{ if $link }}
<div class="flex items-center gap-2">
···
</div>
{{ end }}
{{ end }}
-
{{ if not $profile.IsStatsEmpty }}
<div class="flex items-center justify-evenly gap-2 py-2">
{{ range $stat := .Stats }}
···
{{ end }}
</div>
{{ end }}
-
</div>
{{ end }}
-
{{ if ne .FollowStatus.String "IsSelf" }}
{{ template "user/fragments/follow" . }}
{{ else }}
···
hx-get="/{{ $.UserDid }}/profile/edit-bio"
hx-swap="innerHTML">
{{ i "pencil" "w-4 h-4" }}
-
edit profile
+
edit
</button>
{{ end }}
</div>
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
</div>
</div>
+
</div>
+
{{ end }}
+
+
{{ define "followerFollowing" }}
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+
<span id="followers">{{ .Followers }} followers</span>
+
<span class="select-none after:content-['·']"></span>
+
<span id="following">{{ .Following }} following</span>
</div>
{{ end }}
+1 -80
appview/state/profile.go
···
"fmt"
"log"
"net/http"
-
"net/url"
"slices"
"strings"
···
profile.Stats[1].Kind = db.VanityStatKind(stat1)
}
-
if err := s.validateProfile(profile); err != nil {
+
if err := db.ValidateProfile(s.db, profile); err != nil {
log.Println("invalid profile", err)
s.pages.Notice(w, "update-profile", err.Error())
return
···
s.pages.HxRedirect(w, "/"+user.Did)
return
-
}
-
-
func (s *State) validateProfile(profile *db.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 := db.GetAllReposByDid(s.db, profile.Did)
-
if err != nil {
-
log.Printf("getting repos for %s: %s", profile.Did, err)
-
}
-
-
collaboratingRepos, err := db.CollaboratingIn(s.db, 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 *db.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
}
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
+7 -1
appview/state/state.go
···
jc, err := jetstream.NewJetstreamClient(
config.JetstreamEndpoint,
"appview",
-
[]string{tangled.GraphFollowNSID, tangled.FeedStarNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID},
+
[]string{
+
tangled.GraphFollowNSID,
+
tangled.FeedStarNSID,
+
tangled.PublicKeyNSID,
+
tangled.RepoArtifactNSID,
+
tangled.ActorProfileNSID,
+
},
nil,
slog.Default(),
wrapper,