* fix: remove optimistic UI updates in external actions to prevent race condition (#4711) When adding/deleting external actions, the UI would sometimes show duplicated or cloned rows due to a race condition between: 1. The optimistic setActions() call updating local state 2. The save() callback updating externalActions in context 3. The useEffect syncing actions from externalActions This fix removes the optimistic updates and lets the server response be the single source of truth, updating the UI only after the context is updated via the save() success callback. Fixes #4347 * fix(ap): additional outbound ActivityPub delivery optimizations - check circuit breaker before signing any messages or any other work - create outbound messages in batches and not all at once - add small delay between outbound messages to spread it out a bit * fix(ap): increase ap queue buffer size, reduce outbound http client timeout * fix(ap): handle malformed inbox URIs * fix(ap): batch outbox based on work performed not index * feat(ap): add retryable activitypub outbound delivery client --------- Co-authored-by: John Costa <jcosta@execonline.com>
239 lines
7.4 KiB
Go
239 lines
7.4 KiB
Go
package resolvers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/go-fed/activity/streams"
|
|
"github.com/go-fed/activity/streams/vocab"
|
|
"github.com/owncast/owncast/activitypub/apmodels"
|
|
"github.com/owncast/owncast/activitypub/crypto"
|
|
"github.com/owncast/owncast/persistence/configrepository"
|
|
"github.com/owncast/owncast/utils"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Resolve will translate a raw ActivityPub payload and fire the callback associated with that activity type.
|
|
func Resolve(c context.Context, data []byte, callbacks ...interface{}) error {
|
|
jsonResolver, err := streams.NewJSONResolver(callbacks...)
|
|
if err != nil {
|
|
// Something in the setup was wrong. For example, a callback has an
|
|
// unsupported signature and would never be called
|
|
return err
|
|
}
|
|
|
|
var jsonMap map[string]interface{}
|
|
if err = json.Unmarshal(data, &jsonMap); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugln("Resolving payload...", string(data))
|
|
|
|
// The createCallback function will be called.
|
|
err = jsonResolver.Resolve(c, jsonMap)
|
|
if err != nil && !streams.IsUnmatchedErr(err) {
|
|
// Something went wrong
|
|
return err
|
|
} else if streams.IsUnmatchedErr(err) {
|
|
// Everything went right but the callback didn't match or the ActivityStreams
|
|
// type is one that wasn't code generated.
|
|
log.Debugln("No match: ", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResolveIRI will resolve an IRI and call the correct callback for the resolved type.
|
|
// Uses a retryable HTTP client for resilience against transient failures.
|
|
func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
|
|
configRepository := configrepository.Get()
|
|
log.Debugln("Resolving", iri)
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, iri, nil)
|
|
req.Header.Set("Accept", "application/activity+json, application/ld+json")
|
|
|
|
actor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
|
if err := crypto.SignRequest(req, nil, actor); err != nil {
|
|
return err
|
|
}
|
|
|
|
client := utils.GetRetryableHTTPClient()
|
|
response, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode >= 400 {
|
|
return errors.New("request failed with status: " + response.Status)
|
|
}
|
|
|
|
data, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return Resolve(c, data, callbacks...)
|
|
}
|
|
|
|
// GetResolvedActorFromActorProperty resolve an external actor property to a
|
|
// fully populated internal actor representation.
|
|
// Returns an error if the actor cannot be resolved or is missing required fields.
|
|
func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) {
|
|
var apActor apmodels.ActivityPubActor
|
|
|
|
if actor == nil || actor.Empty() || actor.Len() == 0 || actor.At(0) == nil {
|
|
return apActor, errors.New("actor property is empty or nil")
|
|
}
|
|
|
|
// Explicitly use only the first actor that might be listed.
|
|
actorObjectOrIRI := actor.At(0)
|
|
var actorEntity apmodels.ExternalEntity
|
|
|
|
// If the actor is an unresolved IRI then we need to resolve it.
|
|
if actorObjectOrIRI.IsIRI() {
|
|
iri := actorObjectOrIRI.GetIRI()
|
|
if iri == nil {
|
|
return apActor, errors.New("actor IRI is nil despite IsIRI() returning true")
|
|
}
|
|
return GetResolvedActorFromIRI(iri.String())
|
|
}
|
|
|
|
if actorObjectOrIRI.IsActivityStreamsPerson() {
|
|
actorEntity = actorObjectOrIRI.GetActivityStreamsPerson()
|
|
} else if actorObjectOrIRI.IsActivityStreamsService() {
|
|
actorEntity = actorObjectOrIRI.GetActivityStreamsService()
|
|
} else if actorObjectOrIRI.IsActivityStreamsApplication() {
|
|
actorEntity = actorObjectOrIRI.GetActivityStreamsApplication()
|
|
} else {
|
|
return apActor, errors.New("unrecognized external ActivityPub type: " + actorObjectOrIRI.Name())
|
|
}
|
|
|
|
// Convert the external AP entity into an internal actor representation.
|
|
// NewActivityPubActorFromEntity validates that required fields (ActorIri, Inbox) are present.
|
|
apa, err := apmodels.NewActivityPubActorFromEntity(actorEntity)
|
|
if err != nil {
|
|
return apActor, errors.Wrap(err, "failed to create actor from entity")
|
|
}
|
|
|
|
return *apa, nil
|
|
}
|
|
|
|
// GetResolvedPublicKeyFromIRI will resolve a publicKey IRI string to a vocab.W3IDSecurityV1PublicKey.
|
|
func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1PublicKey, error) {
|
|
var err error
|
|
var pubkey vocab.W3IDSecurityV1PublicKey
|
|
resolved := false
|
|
|
|
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error {
|
|
if pkProp := person.GetW3IDSecurityV1PublicKey(); pkProp != nil {
|
|
for iter := pkProp.Begin(); iter != pkProp.End(); iter = iter.Next() {
|
|
if iter.IsW3IDSecurityV1PublicKey() {
|
|
pubkey = iter.Get()
|
|
resolved = true
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return errors.New("error deriving publickey from activitystreamsperson")
|
|
}
|
|
|
|
serviceCallback := func(c context.Context, service vocab.ActivityStreamsService) error {
|
|
if pkProp := service.GetW3IDSecurityV1PublicKey(); pkProp != nil {
|
|
for iter := pkProp.Begin(); iter != pkProp.End(); iter = iter.Next() {
|
|
if iter.IsW3IDSecurityV1PublicKey() {
|
|
pubkey = iter.Get()
|
|
resolved = true
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return errors.New("error deriving publickey from activitystreamsservice")
|
|
}
|
|
|
|
applicationCallback := func(c context.Context, app vocab.ActivityStreamsApplication) error {
|
|
if pkProp := app.GetW3IDSecurityV1PublicKey(); pkProp != nil {
|
|
for iter := pkProp.Begin(); iter != pkProp.End(); iter = iter.Next() {
|
|
if iter.IsW3IDSecurityV1PublicKey() {
|
|
pubkey = iter.Get()
|
|
resolved = true
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return errors.New("error deriving publickey from activitystreamsapp")
|
|
}
|
|
|
|
pubkeyCallback := func(c context.Context, pk vocab.W3IDSecurityV1PublicKey) error {
|
|
pubkey = pk
|
|
resolved = true
|
|
return nil
|
|
}
|
|
|
|
if e := ResolveIRI(context.Background(), publicKeyIRI, personCallback, serviceCallback, applicationCallback, pubkeyCallback); e != nil {
|
|
err = e
|
|
}
|
|
|
|
if err != nil {
|
|
err = errors.Wrap(err, "error resolving publickey from iri, actor may not be valid: "+publicKeyIRI)
|
|
}
|
|
|
|
if !resolved {
|
|
err = errors.New("error resolving publickey from iri, actor may not be valid: " + publicKeyIRI)
|
|
}
|
|
|
|
return pubkey, err
|
|
}
|
|
|
|
// GetResolvedActorFromIRI will resolve an IRI string to a fully populated actor.
|
|
// Returns an error if the actor cannot be resolved or is missing required fields.
|
|
func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) {
|
|
var resolveErr error
|
|
var apActor apmodels.ActivityPubActor
|
|
resolved := false
|
|
|
|
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error {
|
|
apa, e := apmodels.NewActivityPubActorFromEntity(person)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
apActor = *apa
|
|
resolved = true
|
|
return nil
|
|
}
|
|
|
|
serviceCallback := func(c context.Context, service vocab.ActivityStreamsService) error {
|
|
apa, e := apmodels.NewActivityPubActorFromEntity(service)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
apActor = *apa
|
|
resolved = true
|
|
return nil
|
|
}
|
|
|
|
applicationCallback := func(c context.Context, app vocab.ActivityStreamsApplication) error {
|
|
apa, e := apmodels.NewActivityPubActorFromEntity(app)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
apActor = *apa
|
|
resolved = true
|
|
return nil
|
|
}
|
|
|
|
if e := ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback, applicationCallback); e != nil {
|
|
resolveErr = errors.Wrap(e, "error resolving actor from IRI")
|
|
}
|
|
|
|
if !resolved && resolveErr == nil {
|
|
resolveErr = errors.New("failed to resolve actor from IRI: " + personOrServiceIRI)
|
|
}
|
|
|
|
return apActor, resolveErr
|
|
}
|