First pass at user avatar support. Part of #14 and #15.

This commit is contained in:
Nick Gerakines 2020-03-04 10:28:44 -05:00
parent 99a00f5024
commit 64875a799c
22 changed files with 302 additions and 54 deletions

58
avatar/gen.go Normal file
View File

@ -0,0 +1,58 @@
package avatar
import (
"crypto/md5"
"fmt"
"strconv"
"strings"
"github.com/teacat/noire"
)
const avatarSVG = `<?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"/>
</g>
</svg>
`
func AvatarSVG(input string, size int) string {
primary := toColor(input)
secondary := fmt.Sprintf("#%s", noire.NewHex(primary).Complement().Hex())
height := size
width := size
r := strings.NewReplacer(
"$FIRST", primary,
"$SECOND", fmt.Sprintf("#%s", secondary),
"$WIDTH", strconv.Itoa(width),
"$HEIGHT", strconv.Itoa(height),
)
return r.Replace(avatarSVG)
}
func djb2(data []byte) int32 {
var h int32 = 5381
for _, b := range data {
h = (h << 5) + h + int32(b)
}
return h
}
func toColor(input string) string {
mh := md5.New()
mh.Write([]byte(input))
h := djb2(mh.Sum(nil))
r := (h & 0xff0000) >> 16
g := (h & 0x00ff00) >> 8
b := h & 0x0000ff
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}

18
common/http.go Normal file
View File

@ -0,0 +1,18 @@
package common
import (
"net/http"
"time"
)
// HTTPClient provides the http.Do function in a generic way.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// DefaultHTTPClient returns an HTTPClient with a reasonable default timeout.
func DefaultHTTPClient() HTTPClient {
return &http.Client{
Timeout: 10 * time.Second,
}
}

19
common/reader.go Normal file
View File

@ -0,0 +1,19 @@
package common
import (
"io"
)
type NoOpReaderCloser struct {
Reader io.Reader
}
func (rc NoOpReaderCloser) Read(p []byte) (n int, err error) {
return rc.Reader.Read(p)
}
func (rc NoOpReaderCloser) Close() error {
return nil
}
var _ io.ReadCloser = NoOpReaderCloser{}

31
config/svger.go Normal file
View File

@ -0,0 +1,31 @@
package config
import (
"github.com/urfave/cli/v2"
)
var EnableSVGerFlag = cli.BoolFlag{
Name: "enable-svger",
Usage: "Enable svger integration",
EnvVars: []string{"ENABLE_SVGER"},
Value: false,
}
var SVGerEndpointFlag = cli.StringFlag{
Name: "svger",
Usage: "The SVGer to interact with.",
EnvVars: []string{"SVGER",},
Value: "http://localhost:5003/",
}
type SVGerConfig struct {
Enabled bool
Location string
}
func NewSVGerConfig(cliCtx *cli.Context) SVGerConfig {
return SVGerConfig{
Enabled: cliCtx.Bool("enable-svger"),
Location: cliCtx.String("svger"),
}
}

View File

@ -11,12 +11,13 @@ import (
"github.com/yukimochi/httpsig"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/g"
"github.com/ngerakines/tavern/storage"
)
type ActivityClient struct {
HTTPClient HTTPClient
HTTPClient common.HTTPClient
Logger *zap.Logger
}
@ -43,7 +44,7 @@ func (client ActivityClient) GetSigned(location string, localActor storage.Local
return ldJsonGetSigned(client.HTTPClient, location, signer, localActor.GetKeyID(), privateKey)
}
func ldJsonGet(client HTTPClient, location string) (string, storage.Payload, error) {
func ldJsonGet(client common.HTTPClient, location string) (string, storage.Payload, error) {
request, err := http.NewRequest("GET", location, nil)
if err != nil {
return "", nil, err
@ -73,7 +74,7 @@ func ldJsonGet(client HTTPClient, location string) (string, storage.Payload, err
return string(body), p, nil
}
func ldJsonGetSigned(client HTTPClient, location string, signer httpsig.Signer, keyID string, privateKey *rsa.PrivateKey) (string, storage.Payload, error) {
func ldJsonGetSigned(client common.HTTPClient, location string, signer httpsig.Signer, keyID string, privateKey *rsa.PrivateKey) (string, storage.Payload, error) {
request, err := http.NewRequest("GET", location, nil)
if err != nil {
return "", nil, err

View File

@ -10,11 +10,12 @@ import (
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/storage"
)
type ActorClient struct {
HTTPClient HTTPClient
HTTPClient common.HTTPClient
Logger *zap.Logger
}
@ -49,7 +50,7 @@ func (client ActorClient) Get(location string) (string, storage.Payload, error)
return string(body), p, nil
}
func GetOrFetchActor(ctx context.Context, store storage.Storage, logger *zap.Logger, httpClient HTTPClient, hint string) (storage.Actor, error) {
func GetOrFetchActor(ctx context.Context, store storage.Storage, logger *zap.Logger, httpClient common.HTTPClient, hint string) (storage.Actor, error) {
var actorURL string
if strings.HasPrefix(hint, "https://") {
actorURL = hint

View File

@ -7,11 +7,12 @@ import (
"github.com/yukimochi/httpsig"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/storage"
)
type Crawler struct {
HTTPClient HTTPClient
HTTPClient common.HTTPClient
Logger *zap.Logger
Storage storage.Storage
MaxDepth int

View File

@ -2,21 +2,17 @@ package fed
import (
"fmt"
"net/http"
"net/url"
"strings"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/storage"
)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type WebFingerClient struct {
HTTPClient HTTPClient
HTTPClient common.HTTPClient
Logger *zap.Logger
}

2
go.mod
View File

@ -14,11 +14,13 @@ require (
github.com/gofrs/uuid v3.2.0+incompatible
github.com/kr/pretty v0.1.0
github.com/lib/pq v1.3.0
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/microcosm-cc/bluemonday v1.0.2
github.com/oklog/run v1.0.0
github.com/pkg/errors v0.9.1 // indirect
github.com/sslhound/herr v1.4.1 // indirect
github.com/stretchr/testify v1.4.0
github.com/teacat/noire v1.0.0
github.com/urfave/cli/v2 v2.1.1
github.com/yukimochi/httpsig v0.1.3
go.uber.org/zap v1.13.0

4
go.sum
View File

@ -130,6 +130,8 @@ github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -196,6 +198,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/teacat/noire v1.0.0 h1:+s/oXhDE+HuBUgObpjO3dHEPupo5yiLiUg+7/R6FmYk=
github.com/teacat/noire v1.0.0/go.mod h1:7FDkAv8Eaxqg/9rbSgACb/fVNsTM9khy+rFC6pcnXEE=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=

View File

@ -9,6 +9,7 @@ import (
"github.com/gofrs/uuid"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/storage"
)
@ -19,7 +20,7 @@ type crawl struct {
logger *zap.Logger
queue storage.StringQueue
storage storage.Storage
httpClient HTTPClient
httpClient common.HTTPClient
}
func NewCrawlWorker(logger *zap.Logger, queue storage.StringQueue, storage storage.Storage) Job {
@ -27,7 +28,7 @@ func NewCrawlWorker(logger *zap.Logger, queue storage.StringQueue, storage stora
logger: logger,
queue: queue,
storage: storage,
httpClient: DefaultHTTPClient(),
httpClient: common.DefaultHTTPClient(),
}
}
@ -83,7 +84,7 @@ func (job *crawl) work() error {
}
crawler := &fed.Crawler{
HTTPClient: DefaultHTTPClient(),
HTTPClient: job.httpClient,
Logger: job.logger,
Storage: job.storage,
MaxDepth: fed.CrawlerDefaultMaxCount,

View File

@ -1,12 +0,0 @@
package job
import (
"net/http"
"time"
)
func DefaultHTTPClient() HTTPClient {
return &http.Client{
Timeout: 10 * time.Second,
}
}

View File

@ -2,17 +2,12 @@ package job
import (
"context"
"net/http"
"time"
"github.com/oklog/run"
"go.uber.org/zap"
)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Job interface {
Run(context.Context) error
Shutdown(context.Context) error

View File

@ -7,6 +7,7 @@ import (
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/storage"
)
@ -17,7 +18,7 @@ type webfinger struct {
logger *zap.Logger
queue storage.StringQueue
storage storage.Storage
httpClient HTTPClient
httpClient common.HTTPClient
}
func NewWebFingerWorker(logger *zap.Logger, queue storage.StringQueue, storage storage.Storage) Job {
@ -25,7 +26,7 @@ func NewWebFingerWorker(logger *zap.Logger, queue storage.StringQueue, storage s
logger: logger,
queue: queue,
storage: storage,
httpClient: DefaultHTTPClient(),
httpClient: common.DefaultHTTPClient(),
}
}

View File

@ -48,6 +48,8 @@ var Command = cli.Command{
EnvVars: []string{"ADMIN_NAME"},
Value: "nick",
},
&config.EnableSVGerFlag,
&config.SVGerEndpointFlag,
},
Action: serverCommandAction,
}
@ -76,6 +78,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
if err != nil {
return err
}
svgerConfig := config.NewSVGerConfig(cliCtx)
if sentryConfig.Enabled {
err = sentry.Init(sentry.ClientOptions{
@ -171,6 +174,11 @@ func serverCommandAction(cliCtx *cli.Context) error {
webFingerQueue := storage.NewStringQueue()
crawlQueue := storage.NewStringQueue()
var svgConv SVGConverter
if svgerConfig.Enabled {
svgConv = DefaultSVGerClient(svgerConfig.Location)
}
h := handler{
storage: s,
logger: logger,
@ -180,6 +188,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
crawlQueue: crawlQueue,
adminUser: cliCtx.String("admin-name"),
url: tmplUrlGen(siteBase),
svgConverter: svgConv,
}
configI18nMiddleware(sentryConfig, logger, utrans, domain, r)
@ -201,6 +210,11 @@ 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)
if svgerConfig.Enabled {
root.GET("/avatar/png/:name", h.avatarPNG)
}
authenticated := r.Group("/")
{
configSessionMiddleware(secret, domain, authenticated)

64
web/handle_avatar.go Normal file
View File

@ -0,0 +1,64 @@
package web
import (
"bytes"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/ngerakines/tavern/avatar"
"github.com/ngerakines/tavern/errors"
)
func (h handler) avatarSVG(c *gin.Context) {
svg, err := h.avatar(c)
if err != nil {
h.hardFail(c, err)
return
}
svgReader := bytes.NewReader(svg)
extraHeaders := map[string]string{
"Cache-Control": "max-age=7200",
}
c.DataFromReader(http.StatusOK, svgReader.Size(), "image/svg+xml", svgReader, extraHeaders)
}
func (h handler) avatarPNG(c *gin.Context) {
svg, err := h.avatar(c)
if err != nil {
h.hardFail(c, err)
return
}
readerCloser, length, err := h.svgConverter.Convert(c.Request.Context(), svg)
if err != nil {
h.hardFail(c, err)
return
}
defer readerCloser.Close()
extraHeaders := map[string]string{
"Cache-Control": "max-age=7200",
}
c.DataFromReader(http.StatusOK, length, "image/png", readerCloser, extraHeaders)
}
func (h handler) avatar(c *gin.Context) ([]byte, error) {
name := c.Param("name")
size := intParam(c, "size", 120)
exists, err := h.storage.RowCount(c.Request.Context(), `SELECT COUNT(*) FROM users WHERE name = $1`, name)
if err != nil {
return nil, err
}
if exists == 0 {
return nil, errors.NewNotFoundError(nil)
}
id := fmt.Sprintf("@%s@%s", name, h.domain)
svg := avatar.AvatarSVG(id, size)
return []byte(svg), nil
}

View File

@ -22,9 +22,10 @@ type handler struct {
domain string
sentryConfig config.SentryConfig
webFingerQueue storage.StringQueue
crawlQueue storage.StringQueue
crawlQueue storage.StringQueue
adminUser string
url func(parts ...interface{}) string
svgConverter SVGConverter
}
func (h handler) hardFail(ctx *gin.Context, err error, fields ...zap.Field) {

View File

@ -14,9 +14,9 @@ import (
"github.com/yukimochi/httpsig"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/job"
"github.com/ngerakines/tavern/storage"
)
@ -181,7 +181,7 @@ func (h handler) actorInboxFollow(c *gin.Context, user *storage.User, payload st
if user.AcceptFollowers {
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), target)
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), target)
if err != nil {
h.internalServerErrorJSON(c, err)
return
@ -199,7 +199,7 @@ func (h handler) actorInboxFollow(c *gin.Context, user *storage.User, payload st
responsePayload := response.Bytes()
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: userActorID}, followerActor, responsePayload)
@ -407,7 +407,7 @@ func (h handler) actorInboxAnnounce(c *gin.Context, user *storage.User, payload
}
ac := fed.ActivityClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
localActorID := storage.NewActorID(user.Name, h.domain)

View File

@ -11,9 +11,9 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/job"
"github.com/ngerakines/tavern/storage"
)
@ -103,7 +103,7 @@ func (h handler) createNote(c *gin.Context) {
mentionedActorNames := storage.FindMentionedActors(content)
mentionedActors := make(map[string]storage.Actor)
for _, mentionedActor := range mentionedActorNames {
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), mentionedActor)
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), mentionedActor)
if err == nil {
mentionedActors[mentionedActor] = foundActor
to = append(to, foundActor.GetID())
@ -204,7 +204,7 @@ func (h handler) createNote(c *gin.Context) {
if ok {
continue
}
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), follower)
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), follower)
if err != nil {
continue
}
@ -212,7 +212,7 @@ func (h handler) createNote(c *gin.Context) {
}
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
localActor := storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain)}
@ -290,7 +290,7 @@ func (h handler) announceNote(c *gin.Context) {
}
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.Broadcast(ctx, h.storage, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain)}, announcePayload)

View File

@ -8,9 +8,9 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/job"
"github.com/ngerakines/tavern/storage"
)
@ -105,7 +105,7 @@ func (h handler) networkFollow(c *gin.Context) {
return
}
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), c.PostForm("actor"))
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), c.PostForm("actor"))
if err != nil {
h.flashErrorOrFail(c, "/dashboard/network", err)
return
@ -133,7 +133,7 @@ func (h handler) networkFollow(c *gin.Context) {
fmt.Println(string(payload))
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, actor, payload)
@ -196,7 +196,7 @@ func (h handler) networkUnfollow(c *gin.Context) {
payload := undoFollow.Bytes()
fmt.Println(string(payload))
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), to)
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), to)
if err != nil {
h.flashErrorOrFail(c, "/dashboard/network", err)
return
@ -209,7 +209,7 @@ func (h handler) networkUnfollow(c *gin.Context) {
}
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, actor, payload)
@ -243,7 +243,7 @@ func (h handler) networkAccept(c *gin.Context) {
targetActorID := c.PostForm("actor")
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), targetActorID)
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), targetActorID)
if err != nil {
h.flashErrorOrFail(c, "/dashboard/network", err)
return
@ -266,7 +266,7 @@ func (h handler) networkAccept(c *gin.Context) {
responsePayload := response.Bytes()
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, followerActor, responsePayload)
@ -284,7 +284,6 @@ func (h handler) networkAccept(c *gin.Context) {
c.Redirect(http.StatusFound, "/dashboard/network")
}
func (h handler) networkReject(c *gin.Context) {
session := sessions.Default(c)
ctx := c.Request.Context()
@ -307,7 +306,7 @@ func (h handler) networkReject(c *gin.Context) {
targetActorID := c.PostForm("actor")
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, job.DefaultHTTPClient(), targetActorID)
followerActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), targetActorID)
if err != nil {
h.flashErrorOrFail(c, "/dashboard/network", err)
return
@ -330,7 +329,7 @@ func (h handler) networkReject(c *gin.Context) {
responsePayload := response.Bytes()
nc := fed.ActorClient{
HTTPClient: job.DefaultHTTPClient(),
HTTPClient: common.DefaultHTTPClient(),
Logger: h.logger,
}
err = nc.SendToInbox(ctx, storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain),}, followerActor, responsePayload)

View File

@ -98,6 +98,13 @@ func (h handler) webFinger(c *gin.Context) {
self["href"] = fmt.Sprintf("https://%s/users/%s", domain, user.Name)
response["links"] = []storage.Payload{self}
icon := storage.EmptyPayload()
icon["type"] = "Image"
icon["mediaType"] = "image/png"
icon["url"] = fmt.Sprintf("https://%s/avatar/png/%s", h.domain, user.Name)
self["icon"] = icon
h.writeJRD(c, http.StatusOK, response)
}

47
web/svger.go Normal file
View File

@ -0,0 +1,47 @@
package web
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/g"
)
type SVGConverter interface {
Convert(ctx context.Context, svg []byte) (io.ReadCloser, int64, error)
}
type SVGerClient struct {
HTTPClient common.HTTPClient
Endpoint string
}
func DefaultSVGerClient(endpoint string) SVGConverter {
return SVGerClient{
HTTPClient: common.DefaultHTTPClient(),
Endpoint: endpoint,
}
}
func (c SVGerClient) Convert(ctx context.Context, svg []byte) (io.ReadCloser, int64, error) {
req, err := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewBuffer(svg))
if err != nil {
return nil, 0, err
}
req.Header.Set("Content-Type", "image/svg+xml")
req.Header.Set("User-Agent", g.UserAgent())
response, err := c.HTTPClient.Do(req)
if err != nil {
return nil, 0, err
}
if response.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("unexpected response: %d", response.StatusCode)
}
return response.Body, response.ContentLength, nil
}