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"
)
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">
<svg width="$WIDTH" height="$HEIGHT" viewBox="0 0 $WIDTH $HEIGHT" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g>
@ -24,7 +24,23 @@ const avatarSVG = `<?xml version="1.0" standalone="no"?>
</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)
secondary := fmt.Sprintf("#%s", noire.NewHex(primary).Complement().Hex())
@ -36,7 +52,10 @@ func AvatarSVG(input string, size int) string {
"$WIDTH", strconv.Itoa(width),
"$HEIGHT", strconv.Itoa(height),
)
return r.Replace(avatarSVG)
if local {
return r.Replace(avatarLocalSVG)
}
return r.Replace(avatarRemoteSVG)
}
func djb2(data []byte) int32 {

View File

@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"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")
}
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 {
return nil, err
}
@ -162,7 +174,6 @@ func ActorsFromActivity(activity storage.Payload) []string {
return actors
}
func ActorsFromObject(obj storage.Payload) []string {
var actors []string
@ -190,4 +201,4 @@ func ActorsFromObject(obj storage.Payload) []string {
}
return actors
}
}

View File

@ -2,7 +2,6 @@ package job
import (
"context"
"strings"
"time"
"go.uber.org/zap"
@ -67,53 +66,6 @@ func (job *webfinger) work() error {
return nil
}
var actorID string
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)
_, err = fed.GetOrFetchActor(context.Background(), job.storage, job.logger, job.httpClient, work)
return err
}

View File

@ -18,7 +18,7 @@ import (
type ActorStorage interface {
GetActor(ctx context.Context, id 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)
RecordActorAlias(ctx context.Context, actorID, alias string) 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
}
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()
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 {
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>
<div class="media-body">
<p>
<img src="{{- $actors.Lookup "icon" $note.author -}}?size=14" alt="icon" />
Note by
<a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}}
{{- $actors.Lookup "name" $note.author -}}
</a>
boosted by
<a href="{{ $boost.announcer }}">
{{- lookupStr $actors $boost.announcer -}}
{{- $actors.Lookup "name" $boost.announcer -}}
</a>
on {{ datetime $note.published_at }}
</p>

View File

@ -7,21 +7,22 @@
<i class="fas fa-comment align-self-start fa-2x mr-3 text-secondary"></i>
<div class="media-body">
<p>
<img src="{{- $actors.Lookup "icon" $note.author -}}?size=14" alt="icon" />
{{ if $note.in_reply_to }}
<a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}}
{{- $actors.Lookup "name" $note.author -}}
</a>
replied to
{{ if $note.in_reply_to_author }}
<a href="{{ $note.in_reply_to }}">
{{- lookupStr $actors $note.in_reply_to_author -}}
{{- $actors.Lookup "name" $note.in_reply_to_author -}}
</a>
{{ else }}
<a href="{{ $note.in_reply_to }}"><u>a note</u></a>
{{ end }}
{{ else }}
<a href="{{ $note.author }}">
{{- lookupStr $actors $note.author -}}
{{- $actors.Lookup "name" $note.author -}}
</a> wrote
{{ end }}
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("/tags/:tag", h.getTaggedObjects)
root.GET("/avatar/svg/:name", h.avatarSVG)
root.GET("/avatar/svg/:domain/:name", h.avatarSVG)
if svgerConfig.Enabled {
root.GET("/avatar/png/:name", h.avatarPNG)
root.GET("/avatar/png/:domain/:name", h.avatarPNG)
}
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) {
name := c.Param("name")
domain := c.Param("domain")
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 {
return nil, err
}
@ -58,7 +68,7 @@ func (h handler) avatar(c *gin.Context) ([]byte, error) {
return nil, errors.NewNotFoundError(nil)
}
id := fmt.Sprintf("@%s@%s", name, h.domain)
svg := avatar.AvatarSVG(id, size)
id := fmt.Sprintf("@%s@%s", name, domain)
svg := avatar.AvatarSVG(id, size, domain == h.domain)
return []byte(svg), nil
}

View File

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