mirror of https://gitlab.com/ngerakines/tavern.git
First pass at implementing some activity things
This commit is contained in:
parent
3c98827afc
commit
d0e194ab00
|
@ -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
15
main.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
112
model/actor.go
112
model/actor.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package server
|
208
server/actor.go
208
server/actor.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package server
|
|
@ -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")))
|
||||
|
|
Loading…
Reference in New Issue