mirror of https://gitlab.com/ngerakines/tavern.git
Displaying avatars on feed for actors. Closes #15
This commit is contained in:
parent
64875a799c
commit
78bd51775b
|
@ -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 {
|
||||
|
|
17
fed/actor.go
17
fed/actor.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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("/")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue