aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulio Capote <jcapote@gmail.com>2023-01-07 22:54:38 +0000
committerJulio Capote <jcapote@gmail.com>2023-01-07 22:54:38 +0000
commit14963269dc63bf038163a851e521d6815ab5f514 (patch)
treed85f0b5cfebe22924e0068ce1b47b65c79953331
parentaf05fbea27df62c96b411a941cf5bb612f256e9d (diff)
downloadcommunique-14963269dc63bf038163a851e521d6815ab5f514.tar.gz
avatar support
-rw-r--r--config/config.go12
-rw-r--r--http/router.go12
-rw-r--r--models/avatar.go71
-rw-r--r--registry/registry.go57
-rw-r--r--sample-config.toml4
-rw-r--r--urls/urls.go4
-rw-r--r--views/actor.go21
7 files changed, 172 insertions, 9 deletions
diff --git a/config/config.go b/config/config.go
index 3c6cdde..e92b725 100644
--- a/config/config.go
+++ b/config/config.go
@@ -9,9 +9,11 @@ type Config struct {
}
type Handler struct {
- Name string
- Exec string
- Rpc string
- DedupWindow time.Duration
- Interval time.Duration
+ Name string
+ AvatarUrl string
+ AvatarContentType string
+ Exec string
+ Rpc string
+ DedupWindow time.Duration
+ Interval time.Duration
}
diff --git a/http/router.go b/http/router.go
index 4c251df..6a43499 100644
--- a/http/router.go
+++ b/http/router.go
@@ -2,6 +2,7 @@ package http
import (
"bytes"
+ "fmt"
"io"
"net/http"
@@ -57,6 +58,17 @@ func (s *Router) Start(zapWriter io.Writer) {
render(c, resource, err)
})
+ // Actor avatar
+ router.GET("/actors/:actor/avatar", func(c *gin.Context) {
+ actorParam := c.Param("actor")
+ avatarBytes, mediaType, err := s.registry.ActorAvatar(actorParam)
+ if err != nil || avatarBytes == nil || mediaType == "" {
+ c.Data(404, "text/plain", []byte("404 page not found"))
+ }
+ c.Header("Content-Length", fmt.Sprintf("%d", len(avatarBytes)))
+ c.Data(200, mediaType, avatarBytes)
+ })
+
// Actor Followers
router.GET("/actors/:actor/followers", func(c *gin.Context) {
actorParam := c.Param("actor")
diff --git a/models/avatar.go b/models/avatar.go
new file mode 100644
index 0000000..65c690f
--- /dev/null
+++ b/models/avatar.go
@@ -0,0 +1,71 @@
+package models
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+
+ "git.capotej.com/capotej/communique/config"
+ "github.com/dgraph-io/badger/v3"
+)
+
+type Avatar struct {
+ Handler config.Handler
+ ContentType string
+ Bytes []byte
+}
+
+// used for lookup purposes (count, collect, find)
+func NewAvatar(h config.Handler) *Avatar {
+ aso := &Avatar{Handler: h}
+ return aso
+}
+
+func CreateAvatar(h config.Handler, contentType string, bytes []byte) (*Avatar, error) {
+ aso := &Avatar{
+ Handler: h,
+ ContentType: contentType,
+ Bytes: bytes,
+ }
+ return aso, nil
+}
+
+func (a *Avatar) Name() string {
+ return "Avatar"
+}
+
+func (a *Avatar) Key() string {
+ keyBase := fmt.Sprintf("avatars:%s", a.Handler.Name)
+ return keyBase
+}
+
+func (a *Avatar) DedupKey() string {
+ return a.Key()
+}
+
+func (a *Avatar) Keybase() string {
+ return a.Key()
+}
+
+func (a *Avatar) SaveDedup(txn *badger.Txn) error {
+ txn.Discard() // nothing to do here
+ return nil
+}
+
+func (a *Avatar) Save(txn *badger.Txn) error {
+ if a.Bytes == nil {
+ return fmt.Errorf("bytes not set")
+ }
+ if a.ContentType == "" {
+ return fmt.Errorf("content type not set")
+ }
+
+ var network bytes.Buffer
+ enc := gob.NewEncoder(&network)
+ err := enc.Encode(a)
+ if err != nil {
+ return fmt.Errorf("could not encode Avatar: %w", err)
+ }
+ e := badger.NewEntry([]byte(a.Key()), network.Bytes())
+ return txn.SetEntry(e)
+}
diff --git a/registry/registry.go b/registry/registry.go
index aa61d58..e50c6a5 100644
--- a/registry/registry.go
+++ b/registry/registry.go
@@ -9,6 +9,7 @@ import (
"encoding/pem"
"fmt"
"io"
+ "io/ioutil"
"net/http"
"net/url"
"strings"
@@ -47,6 +48,10 @@ func NewRegistry(cfg config.Config, persister *models.Persister, log *zap.Sugare
if err != nil {
return nil, err
}
+ err = persistAvatarIfFound(v, persister)
+ if err != nil {
+ return nil, err
+ }
}
reg.delivery, err = delivery.NewSigned(persister)
if err != nil {
@@ -67,6 +72,36 @@ func generateKeypairIfNeeded(v config.Handler, p *models.Persister) error {
return nil
}
+func persistAvatarIfFound(v config.Handler, p *models.Persister) error {
+ if v.AvatarUrl == "" || v.AvatarContentType == "" {
+ return nil
+ }
+ resp, err := http.Get(v.AvatarUrl)
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("request to avatarUrl %s failed", v.AvatarUrl)
+ }
+ contentType := resp.Header.Get("content-type")
+ if contentType != v.AvatarContentType {
+ return fmt.Errorf("avatarUrl response content-type '%s' does match avatarContentType '%s'", contentType, v.AvatarContentType)
+ }
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ if len(body) == 0 {
+ return fmt.Errorf("avatarUrl response was empty")
+ }
+ kp, err := models.CreateAvatar(v, v.AvatarContentType, body)
+ if err != nil {
+ return err
+ }
+ err = p.Store(kp)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (r *Registry) Actor(name string) (map[string]interface{}, error) {
handler := r.findByName(name)
if handler == nil {
@@ -91,7 +126,27 @@ func (r *Registry) Actor(name string) (map[string]interface{}, error) {
Bytes: x509.MarshalPKCS1PublicKey(&privKey.PublicKey),
},
)
- return views.RenderActor(handler.handlerCfg.Name, r.cfg.Domain, string(pemdata))
+ return views.RenderActor(handler.handlerCfg.Name, r.cfg.Domain, string(pemdata), handler.handlerCfg.AvatarContentType)
+}
+
+func (r *Registry) ActorAvatar(name string) ([]byte, string, error) {
+ handler := r.findByName(name)
+ if handler == nil {
+ return nil, "", nil
+ }
+ aso := models.NewAvatar(handler.handlerCfg)
+ result, err := r.persister.Find(aso)
+ if err != nil {
+ return nil, "", err
+ }
+ buf := bytes.NewBuffer(result)
+ dec := gob.NewDecoder(buf)
+ var avatar models.Avatar
+ err = dec.Decode(&avatar)
+ if err != nil {
+ return nil, "", err
+ }
+ return avatar.Bytes, handler.handlerCfg.AvatarContentType, nil
}
func (r *Registry) OutboxCollection(name string) (map[string]interface{}, error) {
diff --git a/sample-config.toml b/sample-config.toml
index 9f8a3fa..d724689 100644
--- a/sample-config.toml
+++ b/sample-config.toml
@@ -5,6 +5,8 @@ dbPath = "bub.db"
name = "sample"
rpc = "cgi" # rename to protocol?
exec = "sample-cgi-handler.sh"
+avatarUrl = "https://loremflickr.com/320/240/dog"
+avatarContentType = "image/jpeg"
interval = "1h"
dedupWindow = "5h"
@@ -19,4 +21,4 @@ dedupWindow = "5h"
# name = "another"
# rpc = "cgi"
# exec = "another-cgi-handler.sh"
-# intervalSeconds = "10" \ No newline at end of file
+# intervalSeconds = "10"
diff --git a/urls/urls.go b/urls/urls.go
index 26370a4..69c3336 100644
--- a/urls/urls.go
+++ b/urls/urls.go
@@ -37,6 +37,10 @@ func UrlProfileKey(name, domain string) (*url.URL, error) {
return linkTo("outbox", domain, "actors", name+"#key")
}
+func UrlActorAvatar(name, domain string) (*url.URL, error) {
+ return linkTo("outbox", domain, "actors", name, "avatar")
+}
+
func UrlFollowers(name, domain string) (*url.URL, error) {
return linkTo("outbox", domain, "actors", name, "followers")
}
diff --git a/views/actor.go b/views/actor.go
index 48aaa0b..bc89f95 100644
--- a/views/actor.go
+++ b/views/actor.go
@@ -7,7 +7,7 @@ import (
"github.com/go-fed/activity/streams"
)
-func RenderActor(name, domain, pem string) (map[string]interface{}, error) {
+func RenderActor(name, domain, pem, mediaType string) (map[string]interface{}, error) {
inbox, err := urls.UrlInbox(name, domain)
if err != nil {
return nil, err
@@ -34,6 +34,11 @@ func RenderActor(name, domain, pem string) (map[string]interface{}, error) {
return nil, err
}
+ actorAvatarUrl, err := urls.UrlActorAvatar(name, domain)
+ if err != nil {
+ return nil, err
+ }
+
followingUrl, err := urls.UrlFollowing(name, domain)
if err != nil {
return nil, err
@@ -51,6 +56,18 @@ func RenderActor(name, domain, pem string) (map[string]interface{}, error) {
p.SetActivityStreamsInbox(inboxProp)
p.SetActivityStreamsOutbox(outboxProp)
+ image := streams.NewActivityStreamsImage()
+ mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty()
+ mediaTypeProp.Set(mediaType)
+ image.SetActivityStreamsMediaType(mediaTypeProp)
+ urlProp := streams.NewActivityStreamsUrlProperty()
+ urlProp.AppendIRI(actorAvatarUrl)
+ image.SetActivityStreamsUrl(urlProp)
+
+ iconProp := streams.NewActivityStreamsIconProperty()
+ iconProp.AppendActivityStreamsImage(image)
+ p.SetActivityStreamsIcon(iconProp)
+
nameProp := streams.NewActivityStreamsNameProperty()
nameProp.AppendXMLSchemaString(name)
p.SetActivityStreamsName(nameProp)
@@ -59,7 +76,7 @@ func RenderActor(name, domain, pem string) (map[string]interface{}, error) {
usernameProp.SetXMLSchemaString(name)
p.SetActivityStreamsPreferredUsername(usernameProp)
- urlProp := streams.NewActivityStreamsUrlProperty()
+ urlProp = streams.NewActivityStreamsUrlProperty()
urlProp.AppendIRI(actorUrl)
p.SetActivityStreamsUrl(urlProp)