- Read handler for create activities (notes only so far) - Read handler for note objects - Structure laid out for other objects, activities and collections - DB structure for activities created - Update access logging TODO: Create collections type in DB to describe a collection group
This commit is contained in:
parent
cfa0566c6d
commit
12c9e17c4b
24 changed files with 1327 additions and 206 deletions
|
@ -9,8 +9,14 @@ func BuildActivitypubRouter() http.Handler {
|
|||
router := http.NewServeMux()
|
||||
router.HandleFunc("/user/{id}", users)
|
||||
router.HandleFunc("/user/{id}/inbox", userInbox)
|
||||
router.HandleFunc("/activity/accept/{id}", activityAccept)
|
||||
router.HandleFunc("/activity/create/{id}", activityCreate)
|
||||
router.HandleFunc("/activity/delete/{id}", activityDelete)
|
||||
router.HandleFunc("/activity/reject/{id}", activityReject)
|
||||
router.HandleFunc("/activity/update/{id}", activityUpdate)
|
||||
router.HandleFunc("/note/{id}", objectNote)
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "in ap")
|
||||
_, _ = fmt.Fprint(w, "in ap")
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ func users(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
log := hlog.FromRequest(r)
|
||||
userId := r.PathValue("id")
|
||||
log.Debug().Any("headers", r.Header).Msg("request headers")
|
||||
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).
|
||||
Preload(dbgen.User.Icon).Preload(dbgen.User.Banner).
|
||||
Preload(dbgen.User.BeingTypes).
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
package activitypub
|
||||
|
||||
import "net/http"
|
||||
|
||||
func activityAccept(w http.ResponseWriter, r *http.Request) {}
|
||||
|
|
|
@ -1 +1,23 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Announce is boost
|
||||
|
||||
type activityAnnounceOut struct {
|
||||
Context any `json:"@context,omitempty"`
|
||||
Id string
|
||||
Type string // Always "Announce"
|
||||
Actor string // The one doing the boost
|
||||
Published time.Time
|
||||
To []string
|
||||
CC []string
|
||||
Object string // Link to object being boosted
|
||||
}
|
||||
|
||||
func announceFromStorage(ctx context.Context, id string) (*activityAnnounceOut, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
|
|
@ -1 +1,94 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||
)
|
||||
|
||||
type activityCreateOut struct {
|
||||
Context any `json:"@context,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Actor string `json:"actor"`
|
||||
Object any `json:"object"`
|
||||
}
|
||||
|
||||
func activityCreate(w http.ResponseWriter, r *http.Request) {
|
||||
log := hlog.FromRequest(r)
|
||||
id := r.PathValue("id")
|
||||
activity, err := createFromStorage(r.Context(), id)
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||
case nil:
|
||||
activity.Context = activitypub.BaseLdContext
|
||||
data, err := json.Marshal(activity)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Any("activity", activity).Msg("Failed to marshal create activity")
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/activity+json")
|
||||
fmt.Fprint(w, string(data))
|
||||
default:
|
||||
if storage.HandleReconnectError(err) {
|
||||
log.Error().Err(err).Msg("Connection failed, restart attempt started")
|
||||
} else {
|
||||
log.Error().Err(err).Msg("Failed to get create activity from db")
|
||||
}
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Find a create activity from the db and format it for activitypub reads
|
||||
// Does not set the context for the activity, in case the activity is embedded
|
||||
// in another activity or object. That's the responsibility of the handler
|
||||
// getting the final result
|
||||
func createFromStorage(ctx context.Context, id string) (*activityCreateOut, error) {
|
||||
// log := log.Ctx(ctx)
|
||||
asa := dbgen.ActivitystreamsActivity
|
||||
activity, err := asa.Where(asa.Type.Eq(string(models.ActivityCreate))).
|
||||
Where(asa.Id.Eq(id)).
|
||||
First()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch models.ActivitystreamsActivityTargetType(activity.ObjectType) {
|
||||
case models.ActivitystreamsActivityTargetNote:
|
||||
note, err := noteFromStorage(ctx, activity.ObjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := activityCreateOut{
|
||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id,
|
||||
Type: "Create",
|
||||
Actor: note.AttributedTo,
|
||||
Object: note,
|
||||
}
|
||||
return &out, nil
|
||||
case models.ActivitystreamsActivityTargetBoost:
|
||||
panic("Not implemented")
|
||||
case models.ActivitystreamsActivityTargetReaction:
|
||||
panic("Not implemented")
|
||||
case models.ActivitystreamsActivityTargetActivity:
|
||||
panic("Not implemented")
|
||||
case models.ActivitystreamsActivityTargetUser:
|
||||
panic("Not implemented")
|
||||
case models.ActivitystreamsActivityTargetUnknown:
|
||||
panic("Not implemented")
|
||||
default:
|
||||
panic("Not implemented")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
package activitypub
|
||||
|
||||
import "net/http"
|
||||
|
||||
func activityDelete(w http.ResponseWriter, r *http.Request) {}
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
package activitypub
|
||||
|
||||
import "net/http"
|
||||
|
||||
func activityReject(w http.ResponseWriter, r *http.Request) {}
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
package activitypub
|
||||
|
||||
import "net/http"
|
||||
|
||||
func activityUpdate(w http.ResponseWriter, r *http.Request) {}
|
||||
|
|
30
web/public/api/activitypub/collection.go
Normal file
30
web/public/api/activitypub/collection.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package activitypub
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Used for both unordered and ordered
|
||||
type collectionOut struct {
|
||||
Context any
|
||||
Summary string
|
||||
Type string
|
||||
Items []any
|
||||
Id string
|
||||
TotalItems int
|
||||
First *collectionPageOut
|
||||
}
|
||||
|
||||
// Used for both unordered and ordered
|
||||
type collectionPageOut struct {
|
||||
Context any
|
||||
Type string
|
||||
Id string
|
||||
PartOf string
|
||||
Next string
|
||||
Items []any
|
||||
}
|
||||
|
||||
// Unordered collections handler
|
||||
func collections(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// Ordered collections handler
|
||||
func orderedCollections(w http.ResponseWriter, r *http.Request) {}
|
|
@ -1 +1,102 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||
"git.mstar.dev/mstar/linstrom/config"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||
)
|
||||
|
||||
type objectNoteOut struct {
|
||||
// Context should be set, if needed, by the endpoint handler
|
||||
Context any `json:"@context,omitempty"`
|
||||
|
||||
// Attributes below set from storage
|
||||
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Summary *string `json:"summary"`
|
||||
InReplyTo *string `json:"inReplyTo"`
|
||||
Published time.Time `json:"published"`
|
||||
Url string `json:"url"`
|
||||
AttributedTo string `json:"attributedTo"`
|
||||
To []string `json:"to"`
|
||||
// CC []string `json:"cc"` // FIXME: Uncomment once followers collection implemented
|
||||
Sensitive bool `json:"sensitive"`
|
||||
AtomUri string `json:"atomUri"`
|
||||
InReplyToAtomUri *string `json:"inReplyToAtomUri"`
|
||||
// Conversation string `json:"conversation"` // FIXME: Uncomment once understood what this field wants
|
||||
Content string `json:"content"`
|
||||
// ContentMap map[string]string `json:"content_map"` // TODO: Uncomment once/if support for multiple languages available
|
||||
// Attachments []string `json:"attachments"` // FIXME: Change this to document type
|
||||
// Tags []string `json:"tags"` // FIXME: Change this to hashtag type
|
||||
// Replies any `json:"replies"` // FIXME: Change this to collection type embedding first page
|
||||
// Likes any `json:"likes"` // FIXME: Change this to collection
|
||||
// Shares any `json:"shares"` // FIXME: Change this to collection, is boosts
|
||||
}
|
||||
|
||||
func objectNote(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
log := hlog.FromRequest(r)
|
||||
note, err := noteFromStorage(r.Context(), id)
|
||||
switch err {
|
||||
case gorm.ErrRecordNotFound:
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||
return
|
||||
case nil:
|
||||
note.Context = activitypub.BaseLdContext
|
||||
data, err := json.Marshal(note)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Any("activity", note).Msg("Failed to marshal create activity")
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/activity+json")
|
||||
fmt.Fprint(w, string(data))
|
||||
default:
|
||||
if storage.HandleReconnectError(err) {
|
||||
log.Error().Err(err).Msg("Connection failed, restart attempt started")
|
||||
} else {
|
||||
log.Error().Err(err).Msg("Failed to get create activity from db")
|
||||
}
|
||||
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func noteFromStorage(ctx context.Context, id string) (*objectNoteOut, error) {
|
||||
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).Preload(dbgen.Note.Creator).First()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := &objectNoteOut{
|
||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/note/" + id,
|
||||
Type: "Note",
|
||||
Published: note.CreatedAt,
|
||||
AttributedTo: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/user/" + note.CreatorId,
|
||||
Content: note.RawContent, // FIXME: Escape content
|
||||
Url: config.GlobalConfig.General.GetFullPublicUrl() + "/@" + note.Creator.Username + "/" + id,
|
||||
To: []string{
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
}, // FIXME: Replace with proper targets, not always public
|
||||
AtomUri: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/object/" + id,
|
||||
}
|
||||
if note.RepliesTo.Valid {
|
||||
data.InReplyTo = ¬e.RepliesTo.String
|
||||
data.InReplyToAtomUri = ¬e.RepliesTo.String
|
||||
}
|
||||
if note.ContentWarning.Valid {
|
||||
data.Summary = ¬e.ContentWarning.String
|
||||
data.Sensitive = true
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
@ -64,13 +64,13 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
|
||||
if !config.GlobalConfig.Experimental.AuthFetchForServerActor &&
|
||||
strings.Contains(path, storage.ServerActorId) {
|
||||
log.Info().Msg("Server actor requested, no auth")
|
||||
log.Debug().Msg("Server actor requested, no auth")
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Not an always open path, check methods
|
||||
if r.Method == "GET" && !forGet {
|
||||
log.Info().Msg("Get request to AP resources don't need signature")
|
||||
log.Debug().Msg("Get request to AP resources don't need signature")
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
} else if !forGet && !forNonGet {
|
||||
|
@ -78,7 +78,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
|||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
log.Info().Msg("Need signature for AP request")
|
||||
log.Debug().Msg("Need signature for AP request")
|
||||
rawDate := r.Header.Get("Date")
|
||||
date, err := http.ParseTime(rawDate)
|
||||
if err != nil {
|
||||
|
|
28
web/public/middleware/traceRequestInfo.go
Normal file
28
web/public/middleware/traceRequestInfo.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package webmiddleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
)
|
||||
|
||||
func TraceRequestInfoMiddleware(h http.Handler) http.Handler {
|
||||
return webutils.ChainMiddlewares(
|
||||
h,
|
||||
hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
hlog.FromRequest(r).Trace().Any("headers", r.Header).
|
||||
Bytes("body", body).
|
||||
Str("method", r.Method).
|
||||
Stringer("url", r.URL).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Dur("duration", duration)
|
||||
}),
|
||||
)
|
||||
}
|
|
@ -59,8 +59,13 @@ func New(addr string, duckImg *string, duckFs fs.FS) *Server {
|
|||
server := http.Server{
|
||||
Handler: webutils.ChainMiddlewares(
|
||||
handler,
|
||||
webutils.BuildLoggingMiddleware(map[string]string{"server": "public"}),
|
||||
webutils.BuildLoggingMiddleware(
|
||||
true,
|
||||
[]string{"/assets"},
|
||||
map[string]string{"server": "public"},
|
||||
),
|
||||
webmiddleware.AppendFullPathMiddleware,
|
||||
webmiddleware.TraceRequestInfoMiddleware,
|
||||
),
|
||||
Addr: addr,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue