First pass at implementing some activity things

This commit is contained in:
Nick Gerakines 2019-09-17 17:35:42 -04:00
parent 3c98827afc
commit d0e194ab00
15 changed files with 726 additions and 173 deletions

View File

@ -0,0 +1,12 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello world!",
"published": "2019-09-11T10:30:01Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://tavern.ngrok.io/users/nick/followers"
]
}

15
main.go
View File

@ -108,7 +108,7 @@ func Server(cliCtx *cli.Context) error {
i.Data(200, "text/plain", []byte("OK"))
})
wfh := server.WebfingerHandler{
wkh := server.WebKnownHandler{
Domain: domain,
Logger: logger,
DB: db,
@ -120,7 +120,7 @@ func Server(cliCtx *cli.Context) error {
DB: db,
}
r.GET("/.well-known/webfinger", wfh.Webfinger)
r.GET("/.well-known/webfinger", wkh.WebFinger)
usersRouter := r.Group("/users")
{
@ -129,6 +129,9 @@ func Server(cliCtx *cli.Context) error {
usersRouter.GET("/:user", ah.ActorHandler)
usersRouter.GET("/:user/followers", ah.FollowersHandler)
usersRouter.GET("/:user/following", ah.FollowingHandler)
usersRouter.GET("/:user/outbox", ah.OutboxHandler)
usersRouter.POST("/:user/outbox", ah.OutboxSubmitHandler)
}
var g run.Group
@ -195,7 +198,13 @@ func automigrate(cliCtx *cli.Context, db *gorm.DB, logger *zap.Logger) error {
if err := db.Exec(`CREATE EXTENSION IF NOT EXISTS citext;`).Error; err != nil {
return err
}
if err := db.AutoMigrate(&model.Actor{}, &model.Activity{}, &model.Graph{}).Error; err != nil {
if err := db.AutoMigrate(&model.Actor{},
&model.Activity{},
&model.Graph{},
&model.ActorActivity{},
&model.Object{},
).
Error; err != nil {
return err
}

View File

@ -1,42 +1,26 @@
package model
import (
"database/sql/driver"
"encoding/json"
"github.com/gofrs/uuid"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"time"
)
type Activity struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
ActivityType string `gorm:"not null"`
Actor string `gorm:"not null"`
Object ActivityObject `gorm:"type:jsonb not null default '{}'::jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
}
type ActivityObject map[string]interface{}
func (a ActivityObject) Value() (driver.Value, error) {
return json.Marshal(a)
}
func (a *ActivityObject) Scan(value interface{}) error {
b, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(b, &a)
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
ObjectID string `gorm:"type:varchar(10240);not null;unique"`
Payload JSON `gorm:"type:jsonb not null default '{}'::jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (c *Activity) BeforeCreate(scope *gorm.Scope) error {
id, err := uuid.NewV4()
if err != nil {
return err
if c.ID == uuid.Nil {
id, err := uuid.NewV4()
if err != nil {
return err
}
return scope.SetColumn("ID", id)
}
return scope.SetColumn("ID", id)
return nil
}

View File

@ -13,22 +13,26 @@ import (
)
type Actor struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
Name string `gorm:"not null;unique_index:actors_name"`
Domain string `gorm:"not null"`
Key string `gorm:"not null;type:text"`
CreatedAt time.Time
UpdatedAt time.Time
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
Name string `gorm:"not null;unique_index:actors_name"`
Domain string `gorm:"not null"`
PrivateKey string `gorm:"not null;type:text;default ''"`
PublicKey string `gorm:"not null;type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
type ActorID string
func (c *Actor) BeforeCreate(scope *gorm.Scope) error {
id, err := uuid.NewV4()
if err != nil {
return err
if c.ID == uuid.Nil {
id, err := uuid.NewV4()
if err != nil {
return err
}
return scope.SetColumn("ID", id)
}
return scope.SetColumn("ID", id)
return nil
}
func (ID ActorID) Followers() string {
@ -43,14 +47,26 @@ func (ID ActorID) Following() string {
return fmt.Sprintf("%s/following", ID)
}
func (ID ActorID) FollowingPage(page int) string {
return fmt.Sprintf("%s/following?page=%d", ID, page)
}
func (ID ActorID) Outbox() string {
return fmt.Sprintf("%s/outbox", ID)
}
func (ID ActorID) OutboxPage(page int) string {
return fmt.Sprintf("%s/outbox?page=%d", ID, page)
}
func (ID ActorID) Inbox() string {
return fmt.Sprintf("%s/inbox", ID)
}
func (ID ActorID) MainKey() string {
return fmt.Sprintf("%s#main-key", ID)
}
func NewActorID(name, domain string) ActorID {
return ActorID(fmt.Sprintf("https://%s/users/%s", domain, name))
}
@ -69,38 +85,73 @@ func ActorLookup(db *gorm.DB, name, domain string) (bool, error) {
return userCount == 1, nil
}
func GenerateKey() (string, error) {
func ActorPublicKey(db *gorm.DB, name, domain string) (string, error) {
var keys []string
err := db.
Model(&Actor{}).
Where("name = ? AND domain = ?", name, domain).
Pluck("public_key", &keys).
Error
if err != nil {
return "", err
}
if len(keys) == 0 {
return "", fmt.Errorf("no public keys for user")
}
return keys[0], nil
}
func ActorUUID(db *gorm.DB, name, domain string) (uuid.UUID, error) {
var ids []uuid.UUID
err := db.
Model(&Actor{}).
Where("name = ? AND domain = ?", name, domain).
Pluck("id", &ids).
Error
if err != nil {
return uuid.Nil, err
}
if len(ids) == 0 {
return uuid.Nil, fmt.Errorf("no public keys for user")
}
return ids[0], nil
}
func GenerateKey() (string, string, error) {
reader := rand.Reader
bitSize := 2048
key, err := rsa.GenerateKey(reader, bitSize)
if err != nil {
return "", err
return "", "", err
}
var privateKey = &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
buf := new(bytes.Buffer)
err = pem.Encode(buf, privateKey)
privateKey, err := serializePem("PRIVATE KEY", x509.MarshalPKCS1PrivateKey(key))
if err != nil {
return "", err
return "", "", err
}
return buf.String(), nil
publicKeyB, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return "", "", err
}
publicKey, err := serializePem("PUBLIC KEY", publicKeyB)
if err != nil {
return "", "", err
}
return privateKey, publicKey, nil
}
func CreateActor(db *gorm.DB, name, domain string) (*Actor, error) {
actor := &Actor{}
key, err := GenerateKey()
privateKey, publicKey, err := GenerateKey()
if err != nil {
return nil, err
}
err = db.
Where(Actor{Name: name, Domain: domain}).
Attrs(Actor{Key: key}).
Attrs(Actor{PrivateKey: privateKey, PublicKey: publicKey}).
FirstOrCreate(&actor).
Error
if err != nil {
@ -108,3 +159,18 @@ func CreateActor(db *gorm.DB, name, domain string) (*Actor, error) {
}
return actor, nil
}
func serializePem(pemType string, data []byte) (string, error) {
var privateKey = &pem.Block{
Type: pemType,
Bytes: data,
}
buf := new(bytes.Buffer)
err := pem.Encode(buf, privateKey)
if err != nil {
return "", err
}
return buf.String(), nil
}

59
model/actor_activity.go Normal file
View File

@ -0,0 +1,59 @@
package model
import (
"github.com/gofrs/uuid"
"github.com/jinzhu/gorm"
"time"
)
type ActorActivity struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
Actor Actor `gorm:"foreignkey:ActorID"`
ActorID uuid.UUID `gorm:"not null;type:uuid;unique_index:actor_activity"`
Activity Activity `gorm:"foreignkey:ActivityID"`
ActivityID uuid.UUID `gorm:"not null;type:uuid;unique_index:actor_activity"`
Public bool `gorm:"not null;default false;"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (cs *ActorActivity) BeforeCreate(scope *gorm.Scope) error {
id, err := uuid.NewV4()
if err != nil {
return err
}
return scope.SetColumn("ID", id)
}
func PublicActorActivityCount(db *gorm.DB, actorID uuid.UUID) (int, error) {
var count int
err := db.
Model(&ActorActivity{}).
Where("actor_id = ? AND public = true", actorID).
Count(&count).
Error
if err != nil {
return -1, err
}
return count, nil
}
func PublicActorActivity(db *gorm.DB, actorID uuid.UUID, page, limit int) ([]ActorActivity, error) {
var actorActivity []ActorActivity
err := db.
Where("actor_id = ? AND public = true", actorID).
Order("created_at asc").
Offset((page - 1) * limit).
Limit(limit).
Preload("Activity").
Find(&actorActivity).
Error
if err != nil {
return nil, err
}
return actorActivity, nil
}

68
model/json.go Normal file
View File

@ -0,0 +1,68 @@
package model
import (
"database/sql/driver"
"encoding/json"
"github.com/pkg/errors"
)
type JSON map[string]interface{}
func (a JSON) Value() (driver.Value, error) {
return json.Marshal(a)
}
func (a *JSON) Scan(value interface{}) error {
b, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(b, &a)
}
func JSONMap(document map[string]interface{}, key string) (map[string]interface{}, bool) {
if value, ok := document[key]; ok {
if mapVal, isMap := value.(map[string]interface{}); isMap {
return mapVal, true
}
}
return nil, false
}
func JSONString(document map[string]interface{}, key string) (string, bool) {
if value, ok := document[key]; ok {
if strValue, isString := value.(string); isString {
return strValue, true
}
}
return "", false
}
func JSONStrings(document map[string]interface{}, key string) ([]string, bool) {
var results []string
value, ok := document[key]
if !ok {
return nil, false
}
switch v := value.(type) {
case string:
results = append(results, v)
case []interface{}:
for _, el := range v {
if strValue, isString := el.(string); isString {
results = append(results, strValue)
}
}
}
return results, false
}
func StringsContainsString(things []string, value string) bool {
for _, thing := range things {
if thing == value {
return true
}
}
return false
}

View File

@ -68,13 +68,28 @@ func FollowersPageLookup(db *gorm.DB, to string, page, limit int) ([]string, err
return ids, nil
}
func FollowingLookup(db *gorm.DB, from string) ([]string, error) {
func FollowingCount(db *gorm.DB, from string) (int, error) {
var count int
err := db.
Model(&Graph{}).
Where("follower = ?", from).
Count(&count).
Error
if err != nil {
return -1, err
}
return count, nil
}
func FollowingPageLookup(db *gorm.DB, from string, page, limit int) ([]string, error) {
var ids []string
err := db.
Model(&Graph{}).
Order("created_at asc").
Offset((page-1)*limit).
Limit(limit).
Where("follower = ?", from).
Pluck("actor", &ids).
Pluck("follower", &ids).
Error
if err != nil {
return nil, err

26
model/object.go Normal file
View File

@ -0,0 +1,26 @@
package model
import (
"github.com/gofrs/uuid"
"github.com/jinzhu/gorm"
"time"
)
type Object struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()"`
ObjectID string `gorm:"type:varchar(10240);not null;unique"`
Payload JSON `gorm:"type:jsonb not null default '{}'::jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (c *Object) BeforeCreate(scope *gorm.Scope) error {
if c.ID == uuid.Nil {
id, err := uuid.NewV4()
if err != nil {
return err
}
return scope.SetColumn("ID", id)
}
return nil
}

18
model/tx.go Normal file
View File

@ -0,0 +1,18 @@
package model
import "github.com/jinzhu/gorm"
type TransactionScopedWork func(db *gorm.DB) error
func RunTransactionWithOptions(db *gorm.DB, txBody TransactionScopedWork) error {
tx := db.Begin()
err := txBody(tx)
if err != nil {
if txErr := tx.Rollback().Error; txErr != nil {
return txErr
}
return err
}
return tx.Commit().Error
}

1
server/activity.go Normal file
View File

@ -0,0 +1 @@
package server

View File

@ -1,11 +1,9 @@
package server
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/ngerakines/tavern/model"
"github.com/piprate/json-gold/ld"
"go.uber.org/zap"
"net/http"
"strconv"
@ -17,59 +15,24 @@ type ActorHandler struct {
DB *gorm.DB
}
type collectionResponse struct {
Context string `json:"@context"`
ResponseType string `json:"type"`
ID string `json:"id"`
Total int64 `json:"totalItems"`
Items []string `json:"items,omitempty"`
}
func createFollowersResponse(user, domain string, followers []string) (map[string]interface{}, error) {
/*
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Collection",
"id": "https://tavern.ngrok.io/users/nick/followers",
"totalItems": 0
}
*/
proc := ld.NewJsonLdProcessor()
options := ld.NewJsonLdOptions("https://www.w3.org/ns/activitystreams")
options.ProcessingMode = ld.JsonLd_1_1
actor := model.NewActorID(user, domain)
doc := map[string]interface{}{
"@context": jsonldContextFollowers(),
"type": "Collection",
"id": actor.Followers(),
"totalItems": len(followers),
"items": followers,
}
context := map[string]interface{}{
"@context": jsonldContextFollowers(),
}
return proc.Compact(doc, context, options)
}
func createFollowingResponse(user, domain string, following []string) collectionResponse {
return collectionResponse{
Context: "https://www.w3.org/ns/activitystreams",
ResponseType: "Collection",
ID: fmt.Sprintf("https://%s/users/%s/following", domain, user),
Total: int64(len(following)),
Items: following,
}
}
var (
followersPerPage = 20
followingPerPage = 20
activityPerPage = 20
)
func (h ActorHandler) ActorHandler(c *gin.Context) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
publicKey, err := model.ActorPublicKey(h.DB, user, h.Domain)
if err != nil {
h.Logger.Error("unable to get public key", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
actorContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
@ -107,6 +70,11 @@ func (h ActorHandler) ActorHandler(c *gin.Context) {
"outbox": actor.Outbox(),
"followers": actor.Followers(),
"following": actor.Following(),
"publicKey": map[string]interface{}{
"id": "https://mastodon.social/users/ngerakines#main-key",
"owner": actor.MainKey(),
"publicKeyPem": publicKey,
},
}
documentContext := map[string]interface{}{
@ -181,37 +149,13 @@ func (h ActorHandler) FollowersPageHandler(c *gin.Context) {
page = 1
}
followers, err := model.FollowersPageLookup(h.DB, string(model.NewActorID(user, h.Domain)), page, 20)
followers, err := model.FollowersPageLookup(h.DB, string(model.NewActorID(user, h.Domain)), page, followersPerPage)
if err != nil {
h.Logger.Error("unable to get followers", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
/*
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://mastodon.social/users/ngerakines/following?page=1",
"type": "OrderedCollectionPage",
"totalItems": 20,
"next": "https://mastodon.social/users/ngerakines/following?page=2",
"partOf": "https://mastodon.social/users/ngerakines/following",
"orderedItems": [
"https://mastodon.social/users/Sommer",
"https://mastodon.social/users/ironfroggy",
"https://mastodon.social/users/fribbledom",
"https://mastodon.social/users/ControversyRecords",
"https://mastodon.social/users/envgen",
"https://mastodon.social/users/shesgabrielle",
"https://mastodon.social/users/ottaross",
"https://bsd.network/users/phessler",
"https://toot.cat/users/forktogether",
"https://chaos.social/users/dmitri",
"https://cybre.space/users/qwazix",
"https://mastodon.social/users/dheadshot"
]
}
*/
rootContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]interface{}{
@ -220,8 +164,7 @@ func (h ActorHandler) FollowersPageHandler(c *gin.Context) {
"value": "schema:value",
"orderedItems": map[string]interface{}{
"@container": "@list",
"@id": "as:orderedItems",
//"@type": "@id",
"@id": "as:orderedItems",
},
},
}
@ -230,17 +173,16 @@ func (h ActorHandler) FollowersPageHandler(c *gin.Context) {
"id": actor.FollowersPage(page),
"type": "OrderedCollectionPage",
"totalItems": len(followers),
// next
"partOf": actor.Followers(),
"partOf": actor.Followers(),
}
if len(followers) > 0 {
document["orderedItems"] = followers
}
if len(followers) == 20 {
if len(followers) == followersPerPage {
document["next"] = actor.FollowersPage(page + 1)
}
if page > 1 {
document["last"] = actor.FollowersPage(page - 1)
document["prev"] = actor.FollowersPage(page - 1)
}
documentContext := map[string]interface{}{
@ -258,29 +200,109 @@ func (h ActorHandler) FollowersPageHandler(c *gin.Context) {
}
func (h ActorHandler) FollowingHandler(c *gin.Context) {
if !matchContentType(c) {
c.AbortWithStatus(http.StatusExpectationFailed)
if c.Query("page") == "" {
h.FollowingIndexHandler(c)
return
}
h.FollowingPageHandler(c)
}
func (h ActorHandler) FollowingIndexHandler(c *gin.Context) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
ok, err := model.ActorLookup(h.DB, user, h.Domain)
count, err := model.FollowingCount(h.DB, string(model.NewActorID(user, h.Domain)))
if err != nil {
h.Logger.Error("failed looking up user", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
h.Logger.Error("unable to get following", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if !ok {
h.Logger.Error("user not found", zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusNotFound)
rootContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
}
document := map[string]interface{}{
"@context": rootContext,
"type": "OrderedCollection",
"id": actor.Following(),
"totalItems": count,
}
if count > 0 {
document["first"] = actor.FollowingPage(1)
}
documentContext := map[string]interface{}{
"@context": rootContext,
}
result, err := compactJSONLD(document, documentContext)
if err != nil {
h.Logger.Error("unable to create response", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
following, err := model.FollowingLookup(h.DB, string(model.NewActorID(user, h.Domain)))
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Pragma", "no-cache")
c.JSON(200, createFollowingResponse(user, h.Domain, following))
WriteJSONLD(c, result)
}
func (h ActorHandler) FollowingPageHandler(c *gin.Context) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
h.Logger.Warn("invalid following page", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain), zap.String("page", c.Query("page")))
page = 1
}
if page < 1 {
page = 1
}
followers, err := model.FollowingPageLookup(h.DB, string(model.NewActorID(user, h.Domain)), page, followingPerPage)
if err != nil {
h.Logger.Error("unable to get followers", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
rootContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]interface{}{
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"orderedItems": map[string]interface{}{
"@container": "@list",
"@id": "as:orderedItems",
},
},
}
document := map[string]interface{}{
"@context": rootContext,
"id": actor.FollowingPage(page),
"type": "OrderedCollectionPage",
"totalItems": len(followers),
"partOf": actor.Following(),
}
if len(followers) > 0 {
document["orderedItems"] = followers
}
if len(followers) == followingPerPage {
document["next"] = actor.FollowingPage(page + 1)
}
if page > 1 {
document["prev"] = actor.FollowingPage(page - 1)
}
documentContext := map[string]interface{}{
"@context": rootContext,
}
result, err := compactJSONLD(document, documentContext)
if err != nil {
h.Logger.Error("unable to create response", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
WriteJSONLD(c, result)
}

294
server/actor_outbox.go Normal file
View File

@ -0,0 +1,294 @@
package server
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/ngerakines/tavern/model"
"go.uber.org/zap"
"io/ioutil"
"net/http"
"strconv"
)
func (h ActorHandler) OutboxHandler(c *gin.Context) {
user := c.Param("user")
actorID, err := model.ActorUUID(h.DB, user, h.Domain)
if err != nil {
h.Logger.Error("unable to get actor", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if c.Query("page") == "" {
h.OutboxIndexHandler(c, actorID)
return
}
h.OutboxPageHandler(c, actorID)
}
func (h ActorHandler) OutboxIndexHandler(c *gin.Context, actorID uuid.UUID) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
count, err := model.PublicActorActivityCount(h.DB, actorID)
if err != nil {
h.Logger.Error("unable to get followers", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
rootContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
}
document := map[string]interface{}{
"@context": rootContext,
"type": "OrderedCollection",
"id": actor.Outbox(),
"totalItems": count,
}
if count > 0 {
document["first"] = actor.OutboxPage(1)
}
documentContext := map[string]interface{}{
"@context": rootContext,
}
result, err := compactJSONLD(document, documentContext)
if err != nil {
h.Logger.Error("unable to create response", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
WriteJSONLD(c, result)
}
func (h ActorHandler) OutboxPageHandler(c *gin.Context, actorID uuid.UUID) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
h.Logger.Warn("invalid followers page", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain), zap.String("page", c.Query("page")))
page = 1
}
if page < 1 {
page = 1
}
activity, err := model.PublicActorActivity(h.DB, actorID, page, activityPerPage)
if err != nil {
h.Logger.Error("unable to get followers", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
var activityIDs []string
for _, a := range activity {
activityIDs = append(activityIDs, a.ActivityID.String())
}
rootContext := []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]interface{}{
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"orderedItems": map[string]interface{}{
"@container": "@list",
"@id": "as:orderedItems",
},
},
}
document := map[string]interface{}{
"@context": rootContext,
"id": actor.OutboxPage(page),
"type": "OrderedCollectionPage",
"totalItems": len(activityIDs),
"partOf": actor.Outbox(),
}
if len(activityIDs) > 0 {
document["orderedItems"] = activityIDs
}
if len(activityIDs) == followersPerPage {
document["next"] = actor.OutboxPage(page + 1)
}
if page > 1 {
document["prev"] = actor.OutboxPage(page - 1)
}
documentContext := map[string]interface{}{
"@context": rootContext,
}
result, err := compactJSONLD(document, documentContext)
if err != nil {
h.Logger.Error("unable to create response", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
WriteJSONLD(c, result)
}
func (h ActorHandler) OutboxSubmitHandler(c *gin.Context) {
user := c.Param("user")
actor := model.NewActorID(user, h.Domain)
_, err := model.ActorUUID(h.DB, user, h.Domain)
if err != nil {
h.Logger.Error("unable to get actor", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
var body []byte
var document map[string]interface{}
if body, err = ioutil.ReadAll(c.Request.Body); err != nil {
h.Logger.Error("unable to read request body",
zap.Error(err),
zap.String("user", user),
zap.String("domain", h.Domain),
zap.String("content_type", c.GetHeader("Content-Type")))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if err = json.Unmarshal(body, &document); err != nil {
h.Logger.Error("unable to parse JSON",
zap.Error(err),
zap.String("user", user),
zap.String("domain", h.Domain),
zap.String("content_type", c.GetHeader("Content-Type")))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
documentContext := map[string]interface{}{
"@context": []interface{}{
"https://www.w3.org/ns/activitystreams",
},
}
result, err := compactJSONLD(document, documentContext)
if err != nil {
h.Logger.Error("unable to create response", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if !validateOutboxPayload(result) {
h.Logger.Error("payload did not validate", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain), zap.Any("document", result))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
activityID, _ := uuid.NewV4()
objectID, _ := uuid.NewV4()
obj, err := objectFromPayload(result)
if err != nil {
h.Logger.Error("unable to create object", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
fullActivityID := fmt.Sprintf("https://%s/activity/%s", h.Domain, activityID.String())
result["id"] = fullActivityID
fullObjectID := fmt.Sprintf("https://%s/object/%s", h.Domain, objectID.String())
obj["id"] = fullObjectID
obj["attributedTo"] = string(actor)
//err = model.RunTransactionWithOptions(h.DB, func(tx *gorm.DB) error {
// activityRec := model.Activity{
// ID: activityID,
// ObjectID: fullActivityID,
// Payload: result,
// CreatedAt: time.Time{},
// UpdatedAt: time.Time{},
// }
// if err = tx.Save(&activityRec).Error; err != nil {
// return err
// }
// actorActivityRec := &model.ActorActivity{
// ActorID: actorID,
// ActivityID: activityID,
// Public: true,
// }
// if err = tx.Save(&actorActivityRec).Error; err != nil {
// return err
// }
// objectRec := &model.Object{
// ID: objectID,
// ObjectID: fullObjectID,
// Payload: obj,
// }
// if err = tx.Save(&objectRec).Error; err != nil {
// return err
// }
// return nil
//})
//if err != nil {
// h.Logger.Error("unable to store activity", zap.Error(err), zap.String("user", user), zap.String("domain", h.Domain))
// c.AbortWithStatus(http.StatusInternalServerError)
// return
//}
result["object"] = obj
WriteJSONLD(c, result)
}
func validateOutboxPayload(document map[string]interface{}) bool {
var ok bool
var t string
var published string
var content string
if t, ok = model.JSONString(document, "type"); !ok {
return false
}
if !model.StringsContainsString([]string{"Note"}, t) {
return false
}
if published, ok = model.JSONString(document, "published"); !ok {
return false
}
if len(published) < 1 {
return false
}
if content, ok = model.JSONString(document, "content"); !ok {
return false
}
if len(content) < 1 {
return false
}
return true
}
func objectFromPayload(document map[string]interface{}) (map[string]interface{}, error) {
var obj map[string]interface{}
var ok bool
obj, ok = model.JSONMap(document, "object")
if !ok || obj == nil {
obj = make(map[string]interface{})
}
obj["type"] = document["type"]
obj["content"] = document["content"]
obj["published"] = document["published"]
if _, ok = document["to"]; ok {
obj["to"] = document["to"]
}
if _, ok = document["cc"]; ok {
obj["cc"] = document["cc"]
}
if _, ok = document["bcc"]; ok {
obj["bcc"] = document["bcc"]
}
return obj, nil
}

View File

@ -36,25 +36,3 @@ func matchContentType(c *gin.Context) bool {
return false
}
func jsonldContextFollowers() interface{} {
return []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]interface{}{
"schema": "http://schema.org#",
"items": "as:items",
},
}
}
func jsonldContextFollowering() interface{} {
return []interface{}{
"https://www.w3.org/ns/activitystreams",
map[string]interface{}{
"items": map[string]interface{}{
"@id": "as:items",
"@type": "@id",
},
},
}
}

1
server/object.go Normal file
View File

@ -0,0 +1 @@
package server

View File

@ -13,7 +13,7 @@ import (
type UserLookup func(*gorm.DB, string, string) (bool, error)
type WebfingerHandler struct {
type WebKnownHandler struct {
Domain string
Logger *zap.Logger
DB *gorm.DB
@ -45,7 +45,7 @@ func createWebFingerResponse(user, domain string) webFingerResponse {
}
}
func (h WebfingerHandler) Webfinger(c *gin.Context) {
func (h WebKnownHandler) WebFinger(c *gin.Context) {
user, domain, err := fingerUserDomain(c.Query("resource"), h.Domain)
if err != nil {
h.Logger.Error("failed parsing resource", zap.Error(err), zap.String("resource", c.Query("resource")))