Displaying avatars on feed for actors. Closes #15

This commit is contained in:
Nick Gerakines 2020-03-04 11:31:03 -05:00
parent 64875a799c
commit 78bd51775b
9 changed files with 110 additions and 95 deletions

View File

@ -9,7 +9,7 @@ import (
"github.com/teacat/noire" "github.com/teacat/noire"
) )
const avatarSVG = `<?xml version="1.0" standalone="no"?> const avatarLocalSVG = `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="$WIDTH" height="$HEIGHT" viewBox="0 0 $WIDTH $HEIGHT" version="1.1" xmlns="http://www.w3.org/2000/svg"> <svg width="$WIDTH" height="$HEIGHT" viewBox="0 0 $WIDTH $HEIGHT" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g> <g>
@ -24,7 +24,23 @@ const avatarSVG = `<?xml version="1.0" standalone="no"?>
</svg> </svg>
` `
func AvatarSVG(input string, size int) string { const avatarRemoteSVG = `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="$WIDTH" height="$HEIGHT" viewBox="0 0 $WIDTH $HEIGHT" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="$FIRST"/>
<stop offset="100%" stop-color="$SECOND"/>
</linearGradient>
</defs>
<rect fill="url(#bg)" x="0" y="0" width="$WIDTH" height="$HEIGHT"/>
<polygon points="0,0 $WIDTH,0 $WIDTH,$HEIGHT" fill="#808080" />
</g>
</svg>
`
func AvatarSVG(input string, size int, local bool) string {
primary := toColor(input) primary := toColor(input)
secondary := fmt.Sprintf("#%s", noire.NewHex(primary).Complement().Hex()) secondary := fmt.Sprintf("#%s", noire.NewHex(primary).Complement().Hex())
@ -36,7 +52,10 @@ func AvatarSVG(input string, size int) string {
"$WIDTH", strconv.Itoa(width), "$WIDTH", strconv.Itoa(width),
"$HEIGHT", strconv.Itoa(height), "$HEIGHT", strconv.Itoa(height),
) )
return r.Replace(avatarSVG) if local {
return r.Replace(avatarLocalSVG)
}
return r.Replace(avatarRemoteSVG)
} }
func djb2(data []byte) int32 { func djb2(data []byte) int32 {

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"go.uber.org/zap" "go.uber.org/zap"
@ -103,7 +104,18 @@ func GetOrFetchActor(ctx context.Context, store storage.Storage, logger *zap.Log
return nil, fmt.Errorf("no id found for actor") return nil, fmt.Errorf("no id found for actor")
} }
err = store.CreateActor(ctx, actorRowID, keyRowID, actorID, actorBody, keyID, keyPEM) name, ok := storage.JSONString(actorPayload, "preferredUsername")
if !ok {
return nil, fmt.Errorf("no preferredUsername found for actor")
}
u, err := url.Parse(actorID)
if err != nil {
return nil, err
}
domain := u.Hostname()
err = store.CreateActor(ctx, actorRowID, keyRowID, actorID, actorBody, keyID, keyPEM, name, domain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -162,7 +174,6 @@ func ActorsFromActivity(activity storage.Payload) []string {
return actors return actors
} }
func ActorsFromObject(obj storage.Payload) []string { func ActorsFromObject(obj storage.Payload) []string {
var actors []string var actors []string
@ -190,4 +201,4 @@ func ActorsFromObject(obj storage.Payload) []string {
} }
return actors return actors
} }

View File

@ -2,7 +2,6 @@ package job
import ( import (
"context" "context"
"strings"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
@ -67,53 +66,6 @@ func (job *webfinger) work() error {
return nil return nil
} }
var actorID string _, err = fed.GetOrFetchActor(context.Background(), job.storage, job.logger, job.httpClient, work)
return err
if strings.HasPrefix(work, "https://") {
actorID = work
}
if len(actorID) == 0 {
wfc := fed.WebFingerClient{
HTTPClient: job.httpClient,
Logger: job.logger,
}
wfp, err := wfc.Fetch(work)
if err != nil {
return err
}
actorID, err = fed.ActorIDFromWebFingerPayload(wfp)
if err != nil {
return err
}
job.logger.Debug("parsed actor id from webfinger payload", zap.String("actor", actorID))
}
count, err := job.storage.RowCount(job.ctx, `SELECT COUNT(*) FROM actors WHERE actor_id = $1`, actorID)
if err != nil {
return err
}
if count > 0 {
return nil
}
ac := fed.ActorClient{
HTTPClient: job.httpClient,
Logger: job.logger,
}
actorBody, actorPayload, err := ac.Get(actorID)
if err != nil {
return err
}
keyID, keyPEM, err := storage.KeyFromActor(actorPayload)
actorRowID := storage.NewV4()
keyRowID := storage.NewV4()
return job.storage.CreateActor(context.Background(), actorRowID, keyRowID, actorID, actorBody, keyID, keyPEM)
} }

View File

@ -18,7 +18,7 @@ import (
type ActorStorage interface { type ActorStorage interface {
GetActor(ctx context.Context, id string) (Actor, error) GetActor(ctx context.Context, id string) (Actor, error)
GetActorByAlias(ctx context.Context, alias string) (Actor, error) GetActorByAlias(ctx context.Context, alias string) (Actor, error)
CreateActor(context.Context, uuid.UUID, uuid.UUID, string, string, string, string) error CreateActor(context.Context, uuid.UUID, uuid.UUID, string, string, string, string, string, string) error
GetKey(ctx context.Context, keyID string) (*Key, error) GetKey(ctx context.Context, keyID string) (*Key, error)
RecordActorAlias(ctx context.Context, actorID, alias string) error RecordActorAlias(ctx context.Context, actorID, alias string) error
ActorPayloadsByActorID(ctx context.Context, actorIDs []string) ([]Payload, error) ActorPayloadsByActorID(ctx context.Context, actorIDs []string) ([]Payload, error)
@ -102,10 +102,10 @@ func (s pgStorage) GetKey(ctx context.Context, keyID string) (*Key, error) {
return key, nil return key, nil
} }
func (s pgStorage) CreateActor(ctx context.Context, actorRowID, keyRowID uuid.UUID, actorID, payload, keyID, pem string) error { func (s pgStorage) CreateActor(ctx context.Context, actorRowID, keyRowID uuid.UUID, actorID, payload, keyID, pem, name, domain string) error {
now := s.now() now := s.now()
return runTransactionWithOptions(s.db, func(tx *sql.Tx) error { return runTransactionWithOptions(s.db, func(tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "INSERT INTO actors (id, actor_id, payload, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", actorRowID, actorID, payload, now) _, err := tx.ExecContext(ctx, "INSERT INTO actors (id, actor_id, payload, created_at, name, domain) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING", actorRowID, actorID, payload, now, name, domain)
if err != nil { if err != nil {
return errors.NewInsertQueryFailedError(err) return errors.NewInsertQueryFailedError(err)
} }

View File

@ -8,13 +8,14 @@
<i class="fas fa-megaphone align-self-start fa-2x mr-3 text-secondary"></i> <i class="fas fa-megaphone align-self-start fa-2x mr-3 text-secondary"></i>
<div class="media-body"> <div class="media-body">
<p> <p>
<img src="{{- $actors.Lookup "icon" $note.author -}}?size=14" alt="icon" />
Note by Note by
<a href="{{ $note.author }}"> <a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}} {{- $actors.Lookup "name" $note.author -}}
</a> </a>
boosted by boosted by
<a href="{{ $boost.announcer }}"> <a href="{{ $boost.announcer }}">
{{- lookupStr $actors $boost.announcer -}} {{- $actors.Lookup "name" $boost.announcer -}}
</a> </a>
on {{ datetime $note.published_at }} on {{ datetime $note.published_at }}
</p> </p>

View File

@ -7,21 +7,22 @@
<i class="fas fa-comment align-self-start fa-2x mr-3 text-secondary"></i> <i class="fas fa-comment align-self-start fa-2x mr-3 text-secondary"></i>
<div class="media-body"> <div class="media-body">
<p> <p>
<img src="{{- $actors.Lookup "icon" $note.author -}}?size=14" alt="icon" />
{{ if $note.in_reply_to }} {{ if $note.in_reply_to }}
<a href="{{ $note.author }}"> <a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}} {{- $actors.Lookup "name" $note.author -}}
</a> </a>
replied to replied to
{{ if $note.in_reply_to_author }} {{ if $note.in_reply_to_author }}
<a href="{{ $note.in_reply_to }}"> <a href="{{ $note.in_reply_to }}">
{{- lookupStr $actors $note.in_reply_to_author -}} {{- $actors.Lookup "name" $note.in_reply_to_author -}}
</a> </a>
{{ else }} {{ else }}
<a href="{{ $note.in_reply_to }}"><u>a note</u></a> <a href="{{ $note.in_reply_to }}"><u>a note</u></a>
{{ end }} {{ end }}
{{ else }} {{ else }}
<a href="{{ $note.author }}"> <a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}} {{- $actors.Lookup "name" $note.author -}}
</a> wrote </a> wrote
{{ end }} {{ end }}
on {{ datetime $note.published_at }} on {{ datetime $note.published_at }}

View File

@ -210,9 +210,9 @@ func serverCommandAction(cliCtx *cli.Context) error {
root.GET("/object/:object", h.getObject) root.GET("/object/:object", h.getObject)
root.GET("/tags/:tag", h.getTaggedObjects) root.GET("/tags/:tag", h.getTaggedObjects)
root.GET("/avatar/svg/:name", h.avatarSVG) root.GET("/avatar/svg/:domain/:name", h.avatarSVG)
if svgerConfig.Enabled { if svgerConfig.Enabled {
root.GET("/avatar/png/:name", h.avatarPNG) root.GET("/avatar/png/:domain/:name", h.avatarPNG)
} }
authenticated := r.Group("/") authenticated := r.Group("/")

View File

@ -47,10 +47,20 @@ func (h handler) avatarPNG(c *gin.Context) {
func (h handler) avatar(c *gin.Context) ([]byte, error) { func (h handler) avatar(c *gin.Context) ([]byte, error) {
name := c.Param("name") name := c.Param("name")
domain := c.Param("domain")
size := intParam(c, "size", 120) size := intParam(c, "size", 120)
exists, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM users WHERE name = $1`, name) if len(name) == 0 {
name = domain
domain = h.domain
}
if name == domain && name == "unknown" {
svg := avatar.AvatarSVG("unknown", size, false)
return []byte(svg), nil
}
exists, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM actors WHERE name = $1 AND domain = $2`, name, domain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,7 +68,7 @@ func (h handler) avatar(c *gin.Context) ([]byte, error) {
return nil, errors.NewNotFoundError(nil) return nil, errors.NewNotFoundError(nil)
} }
id := fmt.Sprintf("@%s@%s", name, h.domain) id := fmt.Sprintf("@%s@%s", name, domain)
svg := avatar.AvatarSVG(id, size) svg := avatar.AvatarSVG(id, size, domain == h.domain)
return []byte(svg), nil return []byte(svg), nil
} }

View File

@ -123,7 +123,7 @@ func (h handler) viewFeed(c *gin.Context) {
h.hardFail(c, err) h.hardFail(c, err)
return return
} }
allActors := gatherActors(actors) allActors := h.gatherActors(actors)
var pages []int var pages []int
for i := page - 3; i <= page+3; i++ { for i := page - 3; i <= page+3; i++ {
@ -132,9 +132,8 @@ func (h handler) viewFeed(c *gin.Context) {
} }
} }
data["pages"] = pages data["pages"] = pages
actorNames := actorNames(allActors)
actorNames[string(userActorID)] = "You" data["actors"] = actorLookup{h.domain, allActors}
data["actors"] = actorNames
if cont = h.saveSession(c, session); !cont { if cont = h.saveSession(c, session); !cont {
return return
@ -215,7 +214,7 @@ func (h handler) viewMyFeed(c *gin.Context) {
h.hardFail(c, err) h.hardFail(c, err)
return return
} }
allActors := gatherActors(actors) allActors := h.gatherActors(actors)
var pages []int var pages []int
for i := page - 3; i <= page+3; i++ { for i := page - 3; i <= page+3; i++ {
@ -224,9 +223,7 @@ func (h handler) viewMyFeed(c *gin.Context) {
} }
} }
data["pages"] = pages data["pages"] = pages
actorNames := actorNames(allActors) data["actors"] = actorLookup{h.domain, allActors}
actorNames[string(userActorID)] = "You"
data["actors"] = actorNames
if cont = h.saveSession(c, session); !cont { if cont = h.saveSession(c, session); !cont {
return return
@ -234,7 +231,7 @@ func (h handler) viewMyFeed(c *gin.Context) {
c.HTML(http.StatusOK, "feed", data) c.HTML(http.StatusOK, "feed", data)
} }
func gatherActors(actors []storage.Payload) map[string]map[string]string { func (h handler) gatherActors(actors []storage.Payload) map[string]map[string]string {
results := make(map[string]map[string]string) results := make(map[string]map[string]string)
for _, actor := range actors { for _, actor := range actors {
@ -261,26 +258,50 @@ func gatherActors(actors []storage.Payload) map[string]map[string]string {
summary["domain"] = domain summary["domain"] = domain
summary["at"] = fmt.Sprintf("%s@%s", preferredUsername, domain) summary["at"] = fmt.Sprintf("%s@%s", preferredUsername, domain)
summary["icon"] = fmt.Sprintf("https://%s/avatar/png/%s/%s", h.domain, domain, preferredUsername)
results[actorID] = summary results[actorID] = summary
} }
return results return results
} }
func actorNames(actors map[string]map[string]string) map[string]string { type actorLookup struct {
names := make(map[string]string) domain string
for actorID, actor := range actors { actors map[string]map[string]string
names[actorID] = actorID }
if preferredUsername, ok := actor["preferred_username"]; ok { func (al actorLookup) Lookup(focus string, actorID string) string {
names[actorID] = preferredUsername switch focus {
} case "icon":
if at, ok := actor["at"]; ok { actor, found := al.actors[actorID]
names[actorID] = at if found {
} icon, found := actor["icon"]
if displayName, ok := actor["name"]; ok { if found {
names[actorID] = displayName return icon
} }
} username, foundA := actor["name"]
return names actorDomain, foundB := actor["domain"]
if foundA && foundB {
return fmt.Sprintf("https://%s/avatar/png/%s/%s", al.domain, actorDomain, username)
}
}
return fmt.Sprintf("https://%s/avatar/png/unknown/unknown", al.domain)
case "name":
actor, found := al.actors[actorID]
if found {
if displayName, ok := actor["name"]; ok {
return displayName
}
if at, ok := actor["at"]; ok {
return at
}
if preferredUsername, ok := actor["preferred_username"]; ok {
return preferredUsername
}
}
return actorID
default:
return fmt.Sprintf("unknown key: %s", focus)
}
} }