Add shared inbox support for ActivityPub delivery (#4755)
* feat(ap): add support for shared inboxes to reduce outbound load * feat(db): refactor ap followers db into followers repository * fix(ap): use the updated activity library to pull out the shared inbox endpoint * chore(deps): point at updated build of owncast/activity * fix(ap): typeless endpoints * feat(test): update ActivityPub test to support shared inboxes * chore(test): remove unused variable * fix: feedback from review. Guard against SSRF/non-HTTPS/local and handle transaction errors
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/inbox"
|
"github.com/owncast/owncast/activitypub/inbox"
|
||||||
"github.com/owncast/owncast/activitypub/outbox"
|
"github.com/owncast/owncast/activitypub/outbox"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
|
||||||
@@ -42,8 +43,9 @@ func getOutboundWorkerPoolSize() int {
|
|||||||
defaultWorkers = 20 // Default for most instances
|
defaultWorkers = 20 // Default for most instances
|
||||||
)
|
)
|
||||||
|
|
||||||
|
followersRepo := followersrepository.Get()
|
||||||
var followerCount int64
|
var followerCount int64
|
||||||
fc, err := persistence.GetFollowerCount()
|
fc, err := followersRepo.GetCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Unable to get follower count", err)
|
log.Errorln("Unable to get follower count", err)
|
||||||
return defaultWorkers
|
return defaultWorkers
|
||||||
@@ -58,7 +60,7 @@ func getOutboundWorkerPoolSize() int {
|
|||||||
workers = maxWorkers
|
workers = maxWorkers
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Initializing ActivityPub outbound worker pool with %d workers for %d followers", workers, followerCount)
|
log.Debugf("Initializing ActivityPub outbound worker pool with %d workers for %d followers", workers, followerCount)
|
||||||
return workers
|
return workers
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +81,12 @@ func SendDirectFederatedMessage(message, account string) error {
|
|||||||
|
|
||||||
// GetFollowerCount will return the local tracked follower count.
|
// GetFollowerCount will return the local tracked follower count.
|
||||||
func GetFollowerCount() (int64, error) {
|
func GetFollowerCount() (int64, error) {
|
||||||
return persistence.GetFollowerCount()
|
followersRepo := followersrepository.Get()
|
||||||
|
return followersRepo.GetCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPendingFollowRequests will return the pending follow requests.
|
// GetPendingFollowRequests will return the pending follow requests.
|
||||||
func GetPendingFollowRequests() ([]models.Follower, error) {
|
func GetPendingFollowRequests() ([]models.Follower, error) {
|
||||||
return persistence.GetPendingFollowRequests()
|
followersRepo := followersrepository.Get()
|
||||||
|
return followersRepo.GetPendingFollowRequests()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type ActivityPubActor struct {
|
|||||||
FollowRequestIri *url.URL
|
FollowRequestIri *url.URL
|
||||||
// Inbox is the inbox URL of the remote follower
|
// Inbox is the inbox URL of the remote follower
|
||||||
Inbox *url.URL
|
Inbox *url.URL
|
||||||
|
// SharedInbox is the shared inbox URL of the remote server (optional)
|
||||||
|
SharedInbox *url.URL
|
||||||
// Image is the avatar image of the Actor.
|
// Image is the avatar image of the Actor.
|
||||||
Image *url.URL
|
Image *url.URL
|
||||||
// DisabledAt is the time, if any, this follower was blocked/removed.
|
// DisabledAt is the time, if any, this follower was blocked/removed.
|
||||||
@@ -74,6 +76,14 @@ func (a *ActivityPubActor) InboxString() string {
|
|||||||
return a.Inbox.String()
|
return a.Inbox.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SharedInboxString returns the string representation of SharedInbox, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) SharedInboxString() string {
|
||||||
|
if a.SharedInbox == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.SharedInbox.String()
|
||||||
|
}
|
||||||
|
|
||||||
// ImageString returns the string representation of Image, or empty string if nil.
|
// ImageString returns the string representation of Image, or empty string if nil.
|
||||||
func (a *ActivityPubActor) ImageString() string {
|
func (a *ActivityPubActor) ImageString() string {
|
||||||
if a.Image == nil {
|
if a.Image == nil {
|
||||||
@@ -113,49 +123,68 @@ func NewActivityPubActor(actorIri, inbox *url.URL) (*ActivityPubActor, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateEntityRequiredFields checks that all required fields are present on the entity.
|
||||||
|
func validateEntityRequiredFields(entity ExternalEntity) error {
|
||||||
|
if entity.GetJSONLDId() == nil || entity.GetJSONLDId().Get() == nil {
|
||||||
|
return fmt.Errorf("%w: entity is missing actor IRI", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
if entity.GetActivityStreamsInbox() == nil || entity.GetActivityStreamsInbox().GetIRI() == nil {
|
||||||
|
return fmt.Errorf("%w: entity is missing inbox", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
if entity.GetActivityStreamsPreferredUsername() == nil || entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString() == "" {
|
||||||
|
return fmt.Errorf("%w: entity is missing preferred username", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
if entity.GetW3IDSecurityV1PublicKey() == nil || entity.GetW3IDSecurityV1PublicKey().Len() == 0 {
|
||||||
|
return fmt.Errorf("%w: entity is missing public key", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNameFromEntity extracts the optional name from an entity.
|
||||||
|
func getNameFromEntity(entity ExternalEntity) string {
|
||||||
|
nameProp := entity.GetActivityStreamsName()
|
||||||
|
if nameProp == nil || nameProp.Empty() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return nameProp.At(0).GetXMLSchemaString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSharedInboxFromEntity extracts the optional shared inbox URL from an entity.
|
||||||
|
func getSharedInboxFromEntity(entity ExternalEntity) *url.URL {
|
||||||
|
endpointsProp := entity.GetActivityStreamsEndpoints()
|
||||||
|
if endpointsProp == nil || !endpointsProp.IsActivityStreamsEndpoints() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := endpointsProp.Get()
|
||||||
|
if endpoints == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedInboxProp := endpoints.GetActivityStreamsSharedInbox()
|
||||||
|
if sharedInboxProp == nil || !sharedInboxProp.HasAny() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sharedInboxProp.Get()
|
||||||
|
}
|
||||||
|
|
||||||
// NewActivityPubActorFromEntity creates a new ActivityPubActor from an external entity
|
// NewActivityPubActorFromEntity creates a new ActivityPubActor from an external entity
|
||||||
// with validation of required fields.
|
// with validation of required fields.
|
||||||
func NewActivityPubActorFromEntity(entity ExternalEntity) (*ActivityPubActor, error) {
|
func NewActivityPubActorFromEntity(entity ExternalEntity) (*ActivityPubActor, error) {
|
||||||
// ActorIri is required (must validate before GetFullUsernameFromExternalEntity which uses it)
|
if err := validateEntityRequiredFields(entity); err != nil {
|
||||||
if entity.GetJSONLDId() == nil || entity.GetJSONLDId().Get() == nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("%w: entity is missing actor IRI", ErrActorMissingRequiredField)
|
|
||||||
}
|
}
|
||||||
actorIri := entity.GetJSONLDId().Get()
|
|
||||||
|
|
||||||
// Inbox is required
|
|
||||||
if entity.GetActivityStreamsInbox() == nil || entity.GetActivityStreamsInbox().GetIRI() == nil {
|
|
||||||
return nil, fmt.Errorf("%w: entity is missing inbox", ErrActorMissingRequiredField)
|
|
||||||
}
|
|
||||||
inbox := entity.GetActivityStreamsInbox().GetIRI()
|
|
||||||
|
|
||||||
// Username is required (but not a part of the official ActivityPub spec)
|
|
||||||
if entity.GetActivityStreamsPreferredUsername() == nil || entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString() == "" {
|
|
||||||
return nil, fmt.Errorf("%w: entity is missing preferred username", ErrActorMissingRequiredField)
|
|
||||||
}
|
|
||||||
username := GetFullUsernameFromExternalEntity(entity)
|
|
||||||
|
|
||||||
// Key is required
|
|
||||||
if entity.GetW3IDSecurityV1PublicKey() == nil || entity.GetW3IDSecurityV1PublicKey().Len() == 0 {
|
|
||||||
return nil, fmt.Errorf("%w: entity is missing public key", ErrActorMissingRequiredField)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name is optional
|
|
||||||
var name string
|
|
||||||
if entity.GetActivityStreamsName() != nil && !entity.GetActivityStreamsName().Empty() {
|
|
||||||
name = entity.GetActivityStreamsName().At(0).GetXMLSchemaString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image is optional
|
|
||||||
image := GetImageFromIcon(entity.GetActivityStreamsIcon())
|
|
||||||
|
|
||||||
apActor := &ActivityPubActor{
|
apActor := &ActivityPubActor{
|
||||||
ActorIri: actorIri,
|
ActorIri: entity.GetJSONLDId().Get(),
|
||||||
Inbox: inbox,
|
Inbox: entity.GetActivityStreamsInbox().GetIRI(),
|
||||||
Name: name,
|
SharedInbox: getSharedInboxFromEntity(entity),
|
||||||
|
Name: getNameFromEntity(entity),
|
||||||
Username: entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString(),
|
Username: entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString(),
|
||||||
FullUsername: username,
|
FullUsername: GetFullUsernameFromExternalEntity(entity),
|
||||||
W3IDSecurityV1PublicKey: entity.GetW3IDSecurityV1PublicKey(),
|
W3IDSecurityV1PublicKey: entity.GetW3IDSecurityV1PublicKey(),
|
||||||
Image: image,
|
Image: GetImageFromIcon(entity.GetActivityStreamsIcon()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return apActor, nil
|
return apActor, nil
|
||||||
@@ -174,6 +203,7 @@ type ExternalEntity interface {
|
|||||||
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
|
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
|
||||||
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
|
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
|
||||||
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
||||||
|
GetActivityStreamsEndpoints() vocab.ActivityStreamsEndpointsProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeActorPropertyWithID will return an actor property filled with the provided IRI.
|
// MakeActorPropertyWithID will return an actor property filled with the provided IRI.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,8 @@ func FollowersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getInitialFollowersRequest(r *http.Request) (vocab.ActivityStreamsOrderedCollection, error) {
|
func getInitialFollowersRequest(r *http.Request) (vocab.ActivityStreamsOrderedCollection, error) {
|
||||||
followerCount, _ := persistence.GetFollowerCount()
|
followersRepo := followersrepository.Get()
|
||||||
|
followerCount, _ := followersRepo.GetCount()
|
||||||
collection := streams.NewActivityStreamsOrderedCollection()
|
collection := streams.NewActivityStreamsOrderedCollection()
|
||||||
idProperty := streams.NewJSONLDIdProperty()
|
idProperty := streams.NewJSONLDIdProperty()
|
||||||
id, err := createPageURL(r, nil)
|
id, err := createPageURL(r, nil)
|
||||||
@@ -93,12 +94,13 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere
|
|||||||
return nil, errors.Wrap(err, "unable to parse page number")
|
return nil, errors.Wrap(err, "unable to parse page number")
|
||||||
}
|
}
|
||||||
|
|
||||||
followerCount, err := persistence.GetFollowerCount()
|
followersRepo := followersrepository.Get()
|
||||||
|
followerCount, err := followersRepo.GetCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "unable to get follower count")
|
return nil, errors.Wrap(err, "unable to get follower count")
|
||||||
}
|
}
|
||||||
|
|
||||||
followers, _, err := persistence.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize)
|
followers, _, err := followersRepo.GetFollowers(followersPageSize, (pageInt-1)*followersPageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "unable to get federation followers")
|
return nil, errors.Wrap(err, "unable to get federation followers")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
|
|
||||||
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error {
|
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error {
|
||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
|
followersRepo := followersrepository.Get()
|
||||||
|
|
||||||
follow, err := resolvers.MakeFollowRequest(c, activity)
|
follow, err := resolvers.MakeFollowRequest(c, activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -35,7 +37,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
|||||||
|
|
||||||
followRequest := *follow
|
followRequest := *follow
|
||||||
|
|
||||||
if err := persistence.AddFollow(followRequest, approved); err != nil {
|
if err := followersRepo.Add(followRequest, approved); err != nil {
|
||||||
log.Errorln("unable to save follow request", err)
|
log.Errorln("unable to save follow request", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -95,5 +97,6 @@ func handleUnfollowRequest(c context.Context, activity vocab.ActivityStreamsUndo
|
|||||||
unfollowRequest := *request
|
unfollowRequest := *request
|
||||||
log.Traceln("unfollow request:", unfollowRequest)
|
log.Traceln("unfollow request:", unfollowRequest)
|
||||||
|
|
||||||
return persistence.RemoveFollow(unfollowRequest)
|
followersRepo := followersrepository.Get()
|
||||||
|
return followersRepo.Remove(unfollowRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -22,5 +22,6 @@ func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistence.UpdateFollower(actor.ActorIriString(), actor.InboxString(), actor.Name, actor.FullUsername, actor.ImageString())
|
followersRepo := followersrepository.Get()
|
||||||
|
return followersRepo.Update(actor.ActorIriString(), actor.InboxString(), actor.SharedInboxString(), actor.Name, actor.FullUsername, actor.ImageString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
|
|
||||||
@@ -148,7 +148,8 @@ func isBlockedDomain(domain string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isBlockedActor(actorIRI *url.URL) (bool, error) {
|
func isBlockedActor(actorIRI *url.URL) (bool, error) {
|
||||||
blockedactor, err := persistence.GetFollower(actorIRI.String())
|
followersRepo := followersrepository.Get()
|
||||||
|
blockedactor, err := followersRepo.GetByIRI(actorIRI.String())
|
||||||
|
|
||||||
if blockedactor != nil && blockedactor.DisabledAt != nil {
|
if blockedactor != nil && blockedactor.DisabledAt != nil {
|
||||||
return true, errors.Wrap(err, "remote actor is blocked")
|
return true, errors.Wrap(err, "remote actor is blocked")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
)
|
)
|
||||||
@@ -79,13 +80,14 @@ func TestBlockedDomains(t *testing.T) {
|
|||||||
func TestBlockedActors(t *testing.T) {
|
func TestBlockedActors(t *testing.T) {
|
||||||
person := makeFakePerson()
|
person := makeFakePerson()
|
||||||
fakeRequest := streams.NewActivityStreamsFollow()
|
fakeRequest := streams.NewActivityStreamsFollow()
|
||||||
persistence.AddFollow(apmodels.ActivityPubActor{
|
followersRepo := followersrepository.Get()
|
||||||
|
followersRepo.Add(apmodels.ActivityPubActor{
|
||||||
ActorIri: person.GetJSONLDId().GetIRI(),
|
ActorIri: person.GetJSONLDId().GetIRI(),
|
||||||
Inbox: person.GetJSONLDId().GetIRI(),
|
Inbox: person.GetJSONLDId().GetIRI(),
|
||||||
FollowRequestIri: person.GetJSONLDId().GetIRI(),
|
FollowRequestIri: person.GetJSONLDId().GetIRI(),
|
||||||
RequestObject: fakeRequest,
|
RequestObject: fakeRequest,
|
||||||
}, false)
|
}, false)
|
||||||
persistence.BlockOrRejectFollower(person.GetJSONLDId().GetIRI().String())
|
followersRepo.BlockOrReject(person.GetJSONLDId().GetIRI().String())
|
||||||
|
|
||||||
blocked, err := isBlockedActor(person.GetJSONLDId().GetIRI())
|
blocked, err := isBlockedActor(person.GetJSONLDId().GetIRI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/activitypub/webfinger"
|
"github.com/owncast/owncast/activitypub/webfinger"
|
||||||
@@ -234,15 +235,17 @@ func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
||||||
// Requests are batched to prevent resource exhaustion when there are many followers.
|
// It uses shared inboxes when available to reduce the number of outbound requests.
|
||||||
func SendToFollowers(payload []byte) error {
|
func SendToFollowers(payload []byte) error {
|
||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
|
followersRepo := followersrepository.Get()
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
followers, _, err := persistence.GetFederationFollowers(-1, 0)
|
// Get unique delivery inboxes (prefers shared inboxes over individual inboxes)
|
||||||
|
inboxes, err := followersRepo.GetUniqueDeliveryInboxes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to fetch followers to send to", err)
|
log.Errorln("unable to fetch delivery inboxes", err)
|
||||||
return errors.New("unable to fetch followers to send payload to")
|
return errors.New("unable to fetch delivery inboxes to send payload to")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch size and delay to prevent resource exhaustion during delivery.
|
// Batch size and delay to prevent resource exhaustion during delivery.
|
||||||
@@ -253,10 +256,22 @@ func SendToFollowers(payload []byte) error {
|
|||||||
queued := 0
|
queued := 0
|
||||||
skipped := 0
|
skipped := 0
|
||||||
|
|
||||||
for i, follower := range followers {
|
for i, inboxURL := range inboxes {
|
||||||
inbox, err := url.Parse(follower.Inbox)
|
inbox, err := url.Parse(inboxURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse follower inbox URL", follower.Inbox, err)
|
log.Warnln("unable to parse inbox URL", inboxURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSRF protection: reject non-HTTPS schemes and internal/loopback hosts.
|
||||||
|
// A malicious remote actor could set their inbox to an internal address
|
||||||
|
// to trick this server into making requests to internal services.
|
||||||
|
if inbox.Scheme != "https" {
|
||||||
|
log.Warnln("rejecting non-HTTPS inbox URL for SSRF protection:", inboxURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if utils.IsHostnameInternal(inbox.Hostname()) {
|
||||||
|
log.Warnln("rejecting internal/loopback inbox URL for SSRF protection:", inboxURL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,8 +284,8 @@ func SendToFollowers(payload []byte) error {
|
|||||||
|
|
||||||
req, err := crypto.CreateSignedRequest(payload, inbox, localActor)
|
req, err := crypto.CreateSignedRequest(payload, inbox, localActor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to create outbox request", follower.Inbox, err)
|
log.Errorln("unable to create outbox request", inboxURL, err)
|
||||||
return errors.New("unable to create outbox request: " + follower.Inbox)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
workerpool.AddToOutboundQueue(req)
|
workerpool.AddToOutboundQueue(req)
|
||||||
@@ -280,7 +295,7 @@ func SendToFollowers(payload []byte) error {
|
|||||||
// This helps prevent ActivityPub delivery from competing with video encoding.
|
// This helps prevent ActivityPub delivery from competing with video encoding.
|
||||||
// Use queued count (not loop index) to ensure consistent rate limiting
|
// Use queued count (not loop index) to ensure consistent rate limiting
|
||||||
// even when followers are skipped due to circuit breaker or parse errors.
|
// even when followers are skipped due to circuit breaker or parse errors.
|
||||||
if queued%batchSize == 0 && i+1 < len(followers) {
|
if queued%batchSize == 0 && i+1 < len(inboxes) {
|
||||||
time.Sleep(batchDelay)
|
time.Sleep(batchDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +309,14 @@ func SendToFollowers(payload []byte) error {
|
|||||||
|
|
||||||
// SendToUser will send a payload to a single specific inbox.
|
// SendToUser will send a payload to a single specific inbox.
|
||||||
func SendToUser(inbox *url.URL, payload []byte) error {
|
func SendToUser(inbox *url.URL, payload []byte) error {
|
||||||
|
// SSRF protection: reject non-HTTPS schemes and internal/loopback hosts.
|
||||||
|
if inbox.Scheme != "https" {
|
||||||
|
return errors.Errorf("rejecting non-HTTPS inbox URL for SSRF protection: %s", inbox.String())
|
||||||
|
}
|
||||||
|
if utils.IsHostnameInternal(inbox.Hostname()) {
|
||||||
|
return errors.Errorf("rejecting internal/loopback inbox URL for SSRF protection: %s", inbox.String())
|
||||||
|
}
|
||||||
|
|
||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
package persistence
|
package persistence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +10,7 @@ func createFederationFollowersTable() {
|
|||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_followers (
|
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_followers (
|
||||||
"iri" TEXT NOT NULL,
|
"iri" TEXT NOT NULL,
|
||||||
"inbox" TEXT NOT NULL,
|
"inbox" TEXT NOT NULL,
|
||||||
|
"shared_inbox" TEXT,
|
||||||
"name" TEXT,
|
"name" TEXT,
|
||||||
"username" TEXT NOT NULL,
|
"username" TEXT NOT NULL,
|
||||||
"image" TEXT,
|
"image" TEXT,
|
||||||
@@ -29,91 +24,3 @@ func createFederationFollowersTable() {
|
|||||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_followers (iri);`)
|
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_followers (iri);`)
|
||||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_approved_at ON ap_followers (approved_at);`)
|
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_approved_at ON ap_followers (approved_at);`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowerCount will return the number of followers we're keeping track of.
|
|
||||||
func GetFollowerCount() (int64, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
return _datastore.GetQueries().GetFollowerCount(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationFollowers will return a slice of the followers we keep track of locally.
|
|
||||||
func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
total, err := _datastore.GetQueries().GetFollowerCount(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, errors.Wrap(err, "unable to fetch total number of followers")
|
|
||||||
}
|
|
||||||
|
|
||||||
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range followersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
Inbox: row.Inbox,
|
|
||||||
Timestamp: utils.NullTime(row.CreatedAt),
|
|
||||||
}
|
|
||||||
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, int(total), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPendingFollowRequests will return pending follow requests.
|
|
||||||
func GetPendingFollowRequests() ([]models.Follower, error) {
|
|
||||||
pendingFollowersResult, err := _datastore.GetQueries().GetFederationFollowerApprovalRequests(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range pendingFollowersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
Inbox: row.Inbox,
|
|
||||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
|
||||||
}
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
|
||||||
func GetBlockedAndRejectedFollowers() ([]models.Follower, error) {
|
|
||||||
pendingFollowersResult, err := _datastore.GetQueries().GetRejectedAndBlockedFollowers(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range pendingFollowersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
DisabledAt: utils.NullTime{Time: row.DisabledAt.Time, Valid: true},
|
|
||||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
|
||||||
}
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package persistence
|
package persistence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
@@ -22,16 +27,30 @@ func setup() {
|
|||||||
_datastore = data.GetDatastore()
|
_datastore = data.GetDatastore()
|
||||||
createFederationFollowersTable()
|
createFederationFollowersTable()
|
||||||
|
|
||||||
|
followersRepo := followersrepository.New(_datastore)
|
||||||
|
|
||||||
number := 100
|
number := 100
|
||||||
for i := 0; i < number; i++ {
|
for i := 0; i < number; i++ {
|
||||||
u := createFakeFollower()
|
u := createFakeFollower()
|
||||||
createFollow(u.ActorIRI, u.Inbox, "https://fake.fediverse.server/some/request", u.Name, u.Username, u.Image, nil, true)
|
actorIRI, _ := url.Parse(u.ActorIRI)
|
||||||
|
inboxURL, _ := url.Parse(u.Inbox)
|
||||||
|
requestIRI, _ := url.Parse("https://fake.fediverse.server/some/request")
|
||||||
|
fakeRequest := streams.NewActivityStreamsFollow()
|
||||||
|
followersRepo.Add(apmodels.ActivityPubActor{
|
||||||
|
ActorIri: actorIRI,
|
||||||
|
Inbox: inboxURL,
|
||||||
|
Name: u.Name,
|
||||||
|
Username: u.Username,
|
||||||
|
FollowRequestIri: requestIRI,
|
||||||
|
RequestObject: fakeRequest,
|
||||||
|
}, true)
|
||||||
followers = append(followers, u)
|
followers = append(followers, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowers(t *testing.T) {
|
func TestQueryFollowers(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 0)
|
followersRepo := followersrepository.New(_datastore)
|
||||||
|
f, total, err := followersRepo.GetFollowers(10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
@@ -46,7 +65,8 @@ func TestQueryFollowers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithOffset(t *testing.T) {
|
func TestQueryFollowersWithOffset(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 10)
|
followersRepo := followersrepository.New(_datastore)
|
||||||
|
f, total, err := followersRepo.GetFollowers(10, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
@@ -61,7 +81,8 @@ func TestQueryFollowersWithOffset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 90)
|
followersRepo := followersrepository.New(_datastore)
|
||||||
|
f, total, err := followersRepo.GetFollowers(10, 90)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
@@ -76,7 +97,8 @@ func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithPagination(t *testing.T) {
|
func TestQueryFollowersWithPagination(t *testing.T) {
|
||||||
f, _, err := GetFederationFollowers(15, 10)
|
followersRepo := followersrepository.New(_datastore)
|
||||||
|
f, _, err := followersRepo.GetFollowers(15, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
@@ -105,3 +127,194 @@ func createFakeFollower() models.Follower {
|
|||||||
Timestamp: utils.NullTime{},
|
Timestamp: utils.NullTime{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createTestFollower(followersRepo followersrepository.FollowersRepository, actor, inbox, sharedInbox, request, name, username string) {
|
||||||
|
actorIRI, _ := url.Parse(actor)
|
||||||
|
inboxURL, _ := url.Parse(inbox)
|
||||||
|
var sharedInboxURL *url.URL
|
||||||
|
if sharedInbox != "" {
|
||||||
|
sharedInboxURL, _ = url.Parse(sharedInbox)
|
||||||
|
}
|
||||||
|
requestIRI, _ := url.Parse(request)
|
||||||
|
fakeRequest := streams.NewActivityStreamsFollow()
|
||||||
|
followersRepo.Add(apmodels.ActivityPubActor{
|
||||||
|
ActorIri: actorIRI,
|
||||||
|
Inbox: inboxURL,
|
||||||
|
SharedInbox: sharedInboxURL,
|
||||||
|
Name: name,
|
||||||
|
Username: username,
|
||||||
|
FollowRequestIri: requestIRI,
|
||||||
|
RequestObject: fakeRequest,
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUniqueDeliveryInboxes(t *testing.T) {
|
||||||
|
// Set up a fresh database for this test
|
||||||
|
data.SetupPersistence(":memory:")
|
||||||
|
ds := data.GetDatastore()
|
||||||
|
_datastore = ds
|
||||||
|
createFederationFollowersTable()
|
||||||
|
followersRepo := followersrepository.New(ds)
|
||||||
|
|
||||||
|
// Create followers from server1 with a shared inbox (3 users, 1 shared inbox)
|
||||||
|
server1SharedInbox := "https://server1.example.com/inbox"
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
user, _ := utils.GenerateRandomString(10)
|
||||||
|
createTestFollower(
|
||||||
|
followersRepo,
|
||||||
|
"https://server1.example.com/user/"+user,
|
||||||
|
"https://server1.example.com/user/"+user+"/inbox",
|
||||||
|
server1SharedInbox,
|
||||||
|
"https://server1.example.com/follow/"+user,
|
||||||
|
user,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create followers from server2 with a shared inbox (2 users, 1 shared inbox)
|
||||||
|
server2SharedInbox := "https://server2.example.com/inbox"
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
user, _ := utils.GenerateRandomString(10)
|
||||||
|
createTestFollower(
|
||||||
|
followersRepo,
|
||||||
|
"https://server2.example.com/user/"+user,
|
||||||
|
"https://server2.example.com/user/"+user+"/inbox",
|
||||||
|
server2SharedInbox,
|
||||||
|
"https://server2.example.com/follow/"+user,
|
||||||
|
user,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create followers from server3 WITHOUT a shared inbox (2 users, 2 individual inboxes)
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
user, _ := utils.GenerateRandomString(10)
|
||||||
|
createTestFollower(
|
||||||
|
followersRepo,
|
||||||
|
"https://server3.example.com/user/"+user,
|
||||||
|
"https://server3.example.com/user/"+user+"/inbox",
|
||||||
|
"",
|
||||||
|
"https://server3.example.com/follow/"+user,
|
||||||
|
user,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total: 7 followers, but should result in 4 unique delivery inboxes:
|
||||||
|
// - 1 shared inbox for server1
|
||||||
|
// - 1 shared inbox for server2
|
||||||
|
// - 2 individual inboxes for server3
|
||||||
|
|
||||||
|
inboxes, err := followersRepo.GetUniqueDeliveryInboxes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting unique delivery inboxes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inboxes) != 4 {
|
||||||
|
t.Errorf("Expected 4 unique delivery inboxes, got %d: %v", len(inboxes), inboxes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the shared inboxes are included
|
||||||
|
hasServer1Shared := false
|
||||||
|
hasServer2Shared := false
|
||||||
|
server3IndividualCount := 0
|
||||||
|
|
||||||
|
for _, inbox := range inboxes {
|
||||||
|
if inbox == server1SharedInbox {
|
||||||
|
hasServer1Shared = true
|
||||||
|
}
|
||||||
|
if inbox == server2SharedInbox {
|
||||||
|
hasServer2Shared = true
|
||||||
|
}
|
||||||
|
if len(inbox) > 0 && inbox != server1SharedInbox && inbox != server2SharedInbox {
|
||||||
|
// Should be one of server3's individual inboxes
|
||||||
|
if !strings.Contains(inbox, "server3.example.com") {
|
||||||
|
t.Errorf("Unexpected inbox in results: %s", inbox)
|
||||||
|
}
|
||||||
|
server3IndividualCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasServer1Shared {
|
||||||
|
t.Error("Expected server1 shared inbox to be in results")
|
||||||
|
}
|
||||||
|
if !hasServer2Shared {
|
||||||
|
t.Error("Expected server2 shared inbox to be in results")
|
||||||
|
}
|
||||||
|
if server3IndividualCount != 2 {
|
||||||
|
t.Errorf("Expected 2 individual inboxes from server3, got %d", server3IndividualCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedInboxPreferredOverIndividual(t *testing.T) {
|
||||||
|
// Set up a fresh database for this test
|
||||||
|
data.SetupPersistence(":memory:")
|
||||||
|
ds := data.GetDatastore()
|
||||||
|
_datastore = ds
|
||||||
|
createFederationFollowersTable()
|
||||||
|
followersRepo := followersrepository.New(ds)
|
||||||
|
|
||||||
|
// Create a single follower with both individual and shared inbox
|
||||||
|
sharedInbox := "https://mastodon.social/inbox"
|
||||||
|
individualInbox := "https://mastodon.social/users/testuser/inbox"
|
||||||
|
|
||||||
|
createTestFollower(
|
||||||
|
followersRepo,
|
||||||
|
"https://mastodon.social/users/testuser",
|
||||||
|
individualInbox,
|
||||||
|
sharedInbox,
|
||||||
|
"https://mastodon.social/follow/123",
|
||||||
|
"Test User",
|
||||||
|
"testuser",
|
||||||
|
)
|
||||||
|
|
||||||
|
inboxes, err := followersRepo.GetUniqueDeliveryInboxes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting unique delivery inboxes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inboxes) != 1 {
|
||||||
|
t.Errorf("Expected 1 unique delivery inbox, got %d", len(inboxes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shared inbox should be returned, not the individual inbox
|
||||||
|
if inboxes[0] != sharedInbox {
|
||||||
|
t.Errorf("Expected shared inbox %s, got %s", sharedInbox, inboxes[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndividualInboxUsedWhenNoSharedInbox(t *testing.T) {
|
||||||
|
// Set up a fresh database for this test
|
||||||
|
data.SetupPersistence(":memory:")
|
||||||
|
ds := data.GetDatastore()
|
||||||
|
_datastore = ds
|
||||||
|
createFederationFollowersTable()
|
||||||
|
followersRepo := followersrepository.New(ds)
|
||||||
|
|
||||||
|
// Create a follower without a shared inbox
|
||||||
|
individualInbox := "https://pleroma.example.com/users/testuser/inbox"
|
||||||
|
|
||||||
|
createTestFollower(
|
||||||
|
followersRepo,
|
||||||
|
"https://pleroma.example.com/users/testuser",
|
||||||
|
individualInbox,
|
||||||
|
"",
|
||||||
|
"https://pleroma.example.com/follow/123",
|
||||||
|
"Test User",
|
||||||
|
"testuser",
|
||||||
|
)
|
||||||
|
|
||||||
|
inboxes, err := followersRepo.GetUniqueDeliveryInboxes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting unique delivery inboxes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inboxes) != 1 {
|
||||||
|
t.Errorf("Expected 1 unique delivery inbox, got %d", len(inboxes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The individual inbox should be returned when no shared inbox exists
|
||||||
|
if inboxes[0] != individualInbox {
|
||||||
|
t.Errorf("Expected individual inbox %s, got %s", individualInbox, inboxes[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
package followersrepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/db"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FollowersRepository handles persistence of ActivityPub followers.
|
||||||
|
type FollowersRepository interface {
|
||||||
|
// GetCount returns the number of followers.
|
||||||
|
GetCount() (int64, error)
|
||||||
|
// GetFollowers returns a paginated list of followers.
|
||||||
|
GetFollowers(limit int, offset int) ([]models.Follower, int, error)
|
||||||
|
// GetPendingFollowRequests returns pending follow requests.
|
||||||
|
GetPendingFollowRequests() ([]models.Follower, error)
|
||||||
|
// GetBlockedAndRejected returns blocked and rejected followers.
|
||||||
|
GetBlockedAndRejected() ([]models.Follower, error)
|
||||||
|
// GetUniqueDeliveryInboxes returns unique inbox URLs for delivery.
|
||||||
|
GetUniqueDeliveryInboxes() ([]string, error)
|
||||||
|
// GetByIRI returns a single follower by IRI.
|
||||||
|
GetByIRI(iri string) (*apmodels.ActivityPubActor, error)
|
||||||
|
// Add saves a new follow to the datastore.
|
||||||
|
Add(follow apmodels.ActivityPubActor, approved bool) error
|
||||||
|
// Remove removes a follow from the datastore.
|
||||||
|
Remove(unfollow apmodels.ActivityPubActor) error
|
||||||
|
// ApprovePreviousRequest approves a pending follow request.
|
||||||
|
ApprovePreviousRequest(iri string) error
|
||||||
|
// BlockOrReject blocks an existing follower or rejects a follow request.
|
||||||
|
BlockOrReject(iri string) error
|
||||||
|
// Update updates the details of a stored follower.
|
||||||
|
Update(actorIRI string, inbox string, sharedInbox string, name string, username string, image string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlFollowersRepository is the SQL-based implementation of FollowersRepository.
|
||||||
|
type SqlFollowersRepository struct {
|
||||||
|
datastore *data.Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is temporary during the transition period.
|
||||||
|
var temporaryGlobalInstance FollowersRepository
|
||||||
|
|
||||||
|
// Get returns the followers repository singleton.
|
||||||
|
func Get() FollowersRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
i := New(data.GetDatastore())
|
||||||
|
temporaryGlobalInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the FollowersRepository.
|
||||||
|
func New(datastore *data.Datastore) FollowersRepository {
|
||||||
|
r := SqlFollowersRepository{
|
||||||
|
datastore: datastore,
|
||||||
|
}
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCount returns the number of followers.
|
||||||
|
func (r *SqlFollowersRepository) GetCount() (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
return r.datastore.GetQueries().GetFollowerCount(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFollowers returns a paginated list of followers.
|
||||||
|
func (r *SqlFollowersRepository) GetFollowers(limit int, offset int) ([]models.Follower, int, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
total, err := r.datastore.GetQueries().GetFollowerCount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "unable to fetch total number of followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
followersResult, err := r.datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
||||||
|
Limit: utils.SafeIntToInt32(limit),
|
||||||
|
Offset: utils.SafeIntToInt32(offset),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range followersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
Inbox: row.Inbox,
|
||||||
|
SharedInbox: row.SharedInbox.String,
|
||||||
|
Timestamp: utils.NullTime(row.CreatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, int(total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingFollowRequests returns pending follow requests.
|
||||||
|
func (r *SqlFollowersRepository) GetPendingFollowRequests() ([]models.Follower, error) {
|
||||||
|
pendingFollowersResult, err := r.datastore.GetQueries().GetFederationFollowerApprovalRequests(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range pendingFollowersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
Inbox: row.Inbox,
|
||||||
|
SharedInbox: row.SharedInbox.String,
|
||||||
|
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
||||||
|
}
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockedAndRejected returns blocked and rejected followers.
|
||||||
|
func (r *SqlFollowersRepository) GetBlockedAndRejected() ([]models.Follower, error) {
|
||||||
|
pendingFollowersResult, err := r.datastore.GetQueries().GetRejectedAndBlockedFollowers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range pendingFollowersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
DisabledAt: utils.NullTime{Time: row.DisabledAt.Time, Valid: true},
|
||||||
|
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
||||||
|
}
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUniqueDeliveryInboxes returns unique inbox URLs for delivery.
|
||||||
|
func (r *SqlFollowersRepository) GetUniqueDeliveryInboxes() ([]string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
return r.datastore.GetQueries().GetUniqueDeliveryInboxes(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIRI returns a single follower by IRI.
|
||||||
|
func (r *SqlFollowersRepository) GetByIRI(iri string) (*apmodels.ActivityPubActor, error) {
|
||||||
|
result, err := r.datastore.GetQueries().GetFollowerByIRI(context.Background(), iri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followIRI, err := url.Parse(result.Request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing follow request IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
iriURL, err := url.Parse(result.Iri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing actor IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
inbox, err := url.Parse(result.Inbox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing acting inbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharedInbox *url.URL
|
||||||
|
if result.SharedInbox.Valid && result.SharedInbox.String != "" {
|
||||||
|
sharedInbox, err = url.Parse(result.SharedInbox.String)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("error parsing shared inbox, ignoring:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestObjectBytes := result.RequestObject
|
||||||
|
var followRequestObject vocab.ActivityStreamsFollow
|
||||||
|
|
||||||
|
resolver, err := streams.NewJSONResolver(func(c context.Context, followObject vocab.ActivityStreamsFollow) error {
|
||||||
|
followRequestObject = followObject
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error creating JSON resolver")
|
||||||
|
}
|
||||||
|
jsonMap := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(requestObjectBytes, &jsonMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error unmarshaling follow request object")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resolver.Resolve(context.Background(), jsonMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error resolving follow request object")
|
||||||
|
}
|
||||||
|
|
||||||
|
image, _ := url.Parse(result.Image.String)
|
||||||
|
|
||||||
|
var disabledAt *time.Time
|
||||||
|
if result.DisabledAt.Valid {
|
||||||
|
disabledAt = &result.DisabledAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
follower := apmodels.ActivityPubActor{
|
||||||
|
ActorIri: iriURL,
|
||||||
|
Inbox: inbox,
|
||||||
|
SharedInbox: sharedInbox,
|
||||||
|
Name: result.Name.String,
|
||||||
|
Username: result.Username,
|
||||||
|
Image: image,
|
||||||
|
FollowRequestIri: followIRI,
|
||||||
|
DisabledAt: disabledAt,
|
||||||
|
RequestObject: followRequestObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &follower, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add saves a new follow to the datastore.
|
||||||
|
func (r *SqlFollowersRepository) Add(follow apmodels.ActivityPubActor, approved bool) error {
|
||||||
|
if err := follow.Validate(); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot add invalid follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Traceln("Saving", follow.ActorIriString(), "as a follower.")
|
||||||
|
|
||||||
|
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error serializing follow request object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.createFollow(follow.ActorIriString(), follow.InboxString(), follow.SharedInboxString(), follow.FollowRequestIriString(), follow.Name, follow.Username, follow.ImageString(), followRequestObject, approved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a follow from the datastore.
|
||||||
|
func (r *SqlFollowersRepository) Remove(unfollow apmodels.ActivityPubActor) error {
|
||||||
|
if err := unfollow.Validate(); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot remove invalid follow")
|
||||||
|
}
|
||||||
|
log.Traceln("Removing", unfollow.ActorIriString(), "as a follower.")
|
||||||
|
return r.removeFollow(unfollow.ActorIri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovePreviousRequest approves a pending follow request.
|
||||||
|
func (r *SqlFollowersRepository) ApprovePreviousRequest(iri string) error {
|
||||||
|
return r.datastore.GetQueries().ApproveFederationFollower(context.Background(), db.ApproveFederationFollowerParams{
|
||||||
|
Iri: iri,
|
||||||
|
ApprovedAt: sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockOrReject blocks an existing follower or rejects a follow request.
|
||||||
|
func (r *SqlFollowersRepository) BlockOrReject(iri string) error {
|
||||||
|
return r.datastore.GetQueries().RejectFederationFollower(context.Background(), db.RejectFederationFollowerParams{
|
||||||
|
Iri: iri,
|
||||||
|
DisabledAt: sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the details of a stored follower.
|
||||||
|
func (r *SqlFollowersRepository) Update(actorIRI string, inbox string, sharedInbox string, name string, username string, image string) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error beginning transaction")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = r.datastore.GetQueries().WithTx(tx).UpdateFollowerByIRI(context.Background(), db.UpdateFollowerByIRIParams{
|
||||||
|
Inbox: inbox,
|
||||||
|
SharedInbox: sql.NullString{String: sharedInbox, Valid: sharedInbox != ""},
|
||||||
|
Name: sql.NullString{String: name, Valid: true},
|
||||||
|
Username: username,
|
||||||
|
Image: sql.NullString{String: image, Valid: true},
|
||||||
|
Iri: actorIRI,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "error updating follower "+actorIRI)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlFollowersRepository) createFollow(actor, inbox, sharedInbox, request, name, username, image string, requestObject []byte, approved bool) error {
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error beginning transaction")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var approvedAt sql.NullTime
|
||||||
|
if approved {
|
||||||
|
approvedAt = sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.datastore.GetQueries().WithTx(tx).AddFollower(context.Background(), db.AddFollowerParams{
|
||||||
|
Iri: actor,
|
||||||
|
Inbox: inbox,
|
||||||
|
SharedInbox: sql.NullString{String: sharedInbox, Valid: sharedInbox != ""},
|
||||||
|
Name: sql.NullString{String: name, Valid: true},
|
||||||
|
Username: username,
|
||||||
|
Image: sql.NullString{String: image, Valid: true},
|
||||||
|
ApprovedAt: approvedAt,
|
||||||
|
Request: request,
|
||||||
|
RequestObject: requestObject,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorln("error creating new federation follow: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlFollowersRepository) removeFollow(actor *url.URL) error {
|
||||||
|
r.datastore.DbLock.Lock()
|
||||||
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := r.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := r.datastore.GetQueries().WithTx(tx).RemoveFollowerByIRI(context.Background(), actor.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
@@ -3,18 +3,16 @@ package persistence
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/db"
|
"github.com/owncast/owncast/db"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -31,193 +29,9 @@ func Setup(datastore *data.Datastore) {
|
|||||||
addFollowersFixtureData()
|
addFollowersFixtureData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFollow will save a follow to the datastore.
|
// GetDatastore returns the datastore instance for use by sub-repositories.
|
||||||
func AddFollow(follow apmodels.ActivityPubActor, approved bool) error {
|
func GetDatastore() *data.Datastore {
|
||||||
if err := follow.Validate(); err != nil {
|
return _datastore
|
||||||
return errors.Wrap(err, "cannot add invalid follow")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Traceln("Saving", follow.ActorIriString(), "as a follower.")
|
|
||||||
|
|
||||||
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error serializing follow request object")
|
|
||||||
}
|
|
||||||
|
|
||||||
return createFollow(follow.ActorIriString(), follow.InboxString(), follow.FollowRequestIriString(), follow.Name, follow.Username, follow.ImageString(), followRequestObject, approved)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveFollow will remove a follow from the datastore.
|
|
||||||
func RemoveFollow(unfollow apmodels.ActivityPubActor) error {
|
|
||||||
if err := unfollow.Validate(); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot remove invalid follow")
|
|
||||||
}
|
|
||||||
log.Traceln("Removing", unfollow.ActorIriString(), "as a follower.")
|
|
||||||
return removeFollow(unfollow.ActorIri)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFollower will return a single follower/request given an IRI.
|
|
||||||
func GetFollower(iri string) (*apmodels.ActivityPubActor, error) {
|
|
||||||
result, err := _datastore.GetQueries().GetFollowerByIRI(context.Background(), iri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followIRI, err := url.Parse(result.Request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing follow request IRI")
|
|
||||||
}
|
|
||||||
|
|
||||||
iriURL, err := url.Parse(result.Iri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing actor IRI")
|
|
||||||
}
|
|
||||||
|
|
||||||
inbox, err := url.Parse(result.Inbox)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing acting inbox")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestObjectBytes := result.RequestObject
|
|
||||||
var followRequestObject vocab.ActivityStreamsFollow
|
|
||||||
|
|
||||||
resolver, err := streams.NewJSONResolver(func(c context.Context, followObject vocab.ActivityStreamsFollow) error {
|
|
||||||
followRequestObject = followObject
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error creating JSON resolver")
|
|
||||||
}
|
|
||||||
jsonMap := make(map[string]interface{})
|
|
||||||
err = json.Unmarshal(requestObjectBytes, &jsonMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error unmarshaling follow request object")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resolver.Resolve(context.Background(), jsonMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error resolving follow request object")
|
|
||||||
}
|
|
||||||
|
|
||||||
image, _ := url.Parse(result.Image.String)
|
|
||||||
|
|
||||||
var disabledAt *time.Time
|
|
||||||
if result.DisabledAt.Valid {
|
|
||||||
disabledAt = &result.DisabledAt.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
follower := apmodels.ActivityPubActor{
|
|
||||||
ActorIri: iriURL,
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: result.Name.String,
|
|
||||||
Username: result.Username,
|
|
||||||
Image: image,
|
|
||||||
FollowRequestIri: followIRI,
|
|
||||||
DisabledAt: disabledAt,
|
|
||||||
RequestObject: followRequestObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &follower, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApprovePreviousFollowRequest will approve a follow request.
|
|
||||||
func ApprovePreviousFollowRequest(iri string) error {
|
|
||||||
return _datastore.GetQueries().ApproveFederationFollower(context.Background(), db.ApproveFederationFollowerParams{
|
|
||||||
Iri: iri,
|
|
||||||
ApprovedAt: sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockOrRejectFollower will block an existing follower or reject a follow request.
|
|
||||||
func BlockOrRejectFollower(iri string) error {
|
|
||||||
return _datastore.GetQueries().RejectFederationFollower(context.Background(), db.RejectFederationFollowerParams{
|
|
||||||
Iri: iri,
|
|
||||||
DisabledAt: sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFollow(actor, inbox, request, name, username, image string, requestObject []byte, approved bool) error {
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var approvedAt sql.NullTime
|
|
||||||
if approved {
|
|
||||||
approvedAt = sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = _datastore.GetQueries().WithTx(tx).AddFollower(context.Background(), db.AddFollowerParams{
|
|
||||||
Iri: actor,
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: sql.NullString{String: name, Valid: true},
|
|
||||||
Username: username,
|
|
||||||
Image: sql.NullString{String: image, Valid: true},
|
|
||||||
ApprovedAt: approvedAt,
|
|
||||||
Request: request,
|
|
||||||
RequestObject: requestObject,
|
|
||||||
}); err != nil {
|
|
||||||
log.Errorln("error creating new federation follow: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFollower will update the details of a stored follower given an IRI.
|
|
||||||
func UpdateFollower(actorIRI string, inbox string, name string, username string, image string) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = _datastore.GetQueries().WithTx(tx).UpdateFollowerByIRI(context.Background(), db.UpdateFollowerByIRIParams{
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: sql.NullString{String: name, Valid: true},
|
|
||||||
Username: username,
|
|
||||||
Image: sql.NullString{String: image, Valid: true},
|
|
||||||
Iri: actorIRI,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("error updating follower %s %s", actorIRI, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFollow(actor *url.URL) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := _datastore.GetQueries().WithTx(tx).RemoveFollowerByIRI(context.Background(), actor.String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createFederatedActivitiesTable will create the accepted
|
// createFederatedActivitiesTable will create the accepted
|
||||||
@@ -263,7 +77,7 @@ func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, e
|
|||||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
||||||
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
db.GetOutboxWithOffsetParams{Limit: limit, Offset: offset},
|
db.GetOutboxWithOffsetParams{Limit: utils.SafeIntToInt32(limit), Offset: utils.SafeIntToInt32(offset)},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return collection, err
|
return collection, err
|
||||||
@@ -335,8 +149,8 @@ func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType s
|
|||||||
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
||||||
Limit: limit,
|
Limit: utils.SafeIntToInt32(limit),
|
||||||
Offset: offset,
|
Offset: utils.SafeIntToInt32(offset),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -9,12 +9,22 @@ import (
|
|||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendFollowAccept will send an accept activity to a follow request from a specified local user.
|
// SendFollowAccept will send an accept activity to a follow request from a specified local user.
|
||||||
func SendFollowAccept(inbox *url.URL, originalFollowActivity vocab.ActivityStreamsFollow, fromLocalAccountName string) error {
|
func SendFollowAccept(inbox *url.URL, originalFollowActivity vocab.ActivityStreamsFollow, fromLocalAccountName string) error {
|
||||||
|
// SSRF protection: reject non-HTTPS schemes and internal/loopback hosts.
|
||||||
|
if inbox.Scheme != "https" {
|
||||||
|
return errors.Errorf("rejecting non-HTTPS inbox URL for SSRF protection: %s", inbox.String())
|
||||||
|
}
|
||||||
|
if utils.IsHostnameInternal(inbox.Hostname()) {
|
||||||
|
return errors.Errorf("rejecting internal/loopback inbox URL for SSRF protection: %s", inbox.String())
|
||||||
|
}
|
||||||
|
|
||||||
followAccept := makeAcceptFollow(originalFollowActivity, fromLocalAccountName)
|
followAccept := makeAcceptFollow(originalFollowActivity, fromLocalAccountName)
|
||||||
localAccountIRI := apmodels.MakeLocalIRIForAccount(fromLocalAccountName)
|
localAccountIRI := apmodels.MakeLocalIRIForAccount(fromLocalAccountName)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow)
|
|||||||
ActorIri: person.ActorIri,
|
ActorIri: person.ActorIri,
|
||||||
FollowRequestIri: activity.GetJSONLDId().Get(),
|
FollowRequestIri: activity.GetJSONLDId().Get(),
|
||||||
Inbox: person.Inbox,
|
Inbox: person.Inbox,
|
||||||
|
SharedInbox: person.SharedInbox,
|
||||||
Name: person.Name,
|
Name: person.Name,
|
||||||
Username: fullUsername,
|
Username: fullUsername,
|
||||||
Image: person.Image,
|
Image: person.Image,
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
schemaVersion = 7
|
schemaVersion = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package webhooks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/owncast/owncast/activitypub/events"
|
"github.com/owncast/owncast/activitypub/events"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendFediverseEventFollow will send a user followed event to webhook
|
// SendFediverseEventFollow will send a user followed event to webhook
|
||||||
// destinations.
|
// destinations.
|
||||||
func SendFediverseEngagementFollowEvent(iri string) {
|
func SendFediverseEngagementFollowEvent(iri string) {
|
||||||
follower, err := persistence.GetFollower(iri)
|
followersRepo := followersrepository.Get()
|
||||||
|
follower, err := followersRepo.GetByIRI(iri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.15.0
|
// sqlc v1.30.0
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.15.0
|
// sqlc v1.30.0
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ type ApAcceptedActivity struct {
|
|||||||
type ApFollower struct {
|
type ApFollower struct {
|
||||||
Iri string
|
Iri string
|
||||||
Inbox string
|
Inbox string
|
||||||
|
SharedInbox sql.NullString
|
||||||
Name sql.NullString
|
Name sql.NullString
|
||||||
Username string
|
Username string
|
||||||
Image sql.NullString
|
Image sql.NullString
|
||||||
|
|||||||
+8
-5
@@ -9,13 +9,13 @@ SElECT count(*) FROM ap_followers WHERE approved_at is not null;
|
|||||||
SElECT count(*) FROM ap_outbox;
|
SElECT count(*) FROM ap_outbox;
|
||||||
|
|
||||||
-- name: GetFederationFollowersWithOffset :many
|
-- name: GetFederationFollowersWithOffset :many
|
||||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2;
|
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
-- name: GetRejectedAndBlockedFollowers :many
|
-- name: GetRejectedAndBlockedFollowers :many
|
||||||
SELECT iri, name, username, image, created_at, disabled_at FROM ap_followers WHERE disabled_at is not null;
|
SELECT iri, name, username, image, created_at, disabled_at FROM ap_followers WHERE disabled_at is not null;
|
||||||
|
|
||||||
-- name: GetFederationFollowerApprovalRequests :many
|
-- name: GetFederationFollowerApprovalRequests :many
|
||||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null;
|
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null;
|
||||||
|
|
||||||
-- name: ApproveFederationFollower :exec
|
-- name: ApproveFederationFollower :exec
|
||||||
UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2;
|
UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2;
|
||||||
@@ -24,7 +24,7 @@ UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2;
|
|||||||
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2;
|
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2;
|
||||||
|
|
||||||
-- name: GetFollowerByIRI :one
|
-- name: GetFollowerByIRI :one
|
||||||
SELECT iri, inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1;
|
SELECT iri, inbox, shared_inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1;
|
||||||
|
|
||||||
-- name: GetOutboxWithOffset :many
|
-- name: GetOutboxWithOffset :many
|
||||||
SELECT value FROM ap_outbox LIMIT $1 OFFSET $2;
|
SELECT value FROM ap_outbox LIMIT $1 OFFSET $2;
|
||||||
@@ -37,7 +37,7 @@ SELECT value, live_notification, created_at FROM ap_outbox WHERE iri = $1;
|
|||||||
DELETE FROM ap_followers WHERE iri = $1;
|
DELETE FROM ap_followers WHERE iri = $1;
|
||||||
|
|
||||||
-- name: AddFollower :exec
|
-- name: AddFollower :exec
|
||||||
INSERT INTO ap_followers(iri, inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8);
|
INSERT INTO ap_followers(iri, inbox, shared_inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
||||||
|
|
||||||
-- name: AddToOutbox :exec
|
-- name: AddToOutbox :exec
|
||||||
INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4);
|
INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4);
|
||||||
@@ -55,7 +55,10 @@ SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestam
|
|||||||
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3;
|
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3;
|
||||||
|
|
||||||
-- name: UpdateFollowerByIRI :exec
|
-- name: UpdateFollowerByIRI :exec
|
||||||
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5;
|
UPDATE ap_followers SET inbox = $1, shared_inbox = $2, name = $3, username = $4, image = $5 WHERE iri = $6;
|
||||||
|
|
||||||
|
-- name: GetUniqueDeliveryInboxes :many
|
||||||
|
SELECT COALESCE(shared_inbox, inbox) as delivery_inbox FROM ap_followers WHERE approved_at is not null GROUP BY delivery_inbox;
|
||||||
|
|
||||||
-- name: BanIPAddress :exec
|
-- name: BanIPAddress :exec
|
||||||
INSERT INTO ip_bans(ip_address, notes) values($1, $2);
|
INSERT INTO ip_bans(ip_address, notes) values($1, $2);
|
||||||
|
|||||||
+66
-30
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.19.1
|
// sqlc v1.30.0
|
||||||
// source: query.sql
|
// source: query.sql
|
||||||
|
|
||||||
package db
|
package db
|
||||||
@@ -41,12 +41,13 @@ func (q *Queries) AddAuthForUser(ctx context.Context, arg AddAuthForUserParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addFollower = `-- name: AddFollower :exec
|
const addFollower = `-- name: AddFollower :exec
|
||||||
INSERT INTO ap_followers(iri, inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8)
|
INSERT INTO ap_followers(iri, inbox, shared_inbox, request, request_object, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
`
|
`
|
||||||
|
|
||||||
type AddFollowerParams struct {
|
type AddFollowerParams struct {
|
||||||
Iri string
|
Iri string
|
||||||
Inbox string
|
Inbox string
|
||||||
|
SharedInbox sql.NullString
|
||||||
Request string
|
Request string
|
||||||
RequestObject []byte
|
RequestObject []byte
|
||||||
Name sql.NullString
|
Name sql.NullString
|
||||||
@@ -59,6 +60,7 @@ func (q *Queries) AddFollower(ctx context.Context, arg AddFollowerParams) error
|
|||||||
_, err := q.db.ExecContext(ctx, addFollower,
|
_, err := q.db.ExecContext(ctx, addFollower,
|
||||||
arg.Iri,
|
arg.Iri,
|
||||||
arg.Inbox,
|
arg.Inbox,
|
||||||
|
arg.SharedInbox,
|
||||||
arg.Request,
|
arg.Request,
|
||||||
arg.RequestObject,
|
arg.RequestObject,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
@@ -158,7 +160,7 @@ UPDATE users SET display_color = $1 WHERE id = $2
|
|||||||
`
|
`
|
||||||
|
|
||||||
type ChangeDisplayColorParams struct {
|
type ChangeDisplayColorParams struct {
|
||||||
DisplayColor int
|
DisplayColor int32
|
||||||
ID string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,16 +208,17 @@ func (q *Queries) DoesInboundActivityExist(ctx context.Context, arg DoesInboundA
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFederationFollowerApprovalRequests = `-- name: GetFederationFollowerApprovalRequests :many
|
const getFederationFollowerApprovalRequests = `-- name: GetFederationFollowerApprovalRequests :many
|
||||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null
|
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetFederationFollowerApprovalRequestsRow struct {
|
type GetFederationFollowerApprovalRequestsRow struct {
|
||||||
Iri string
|
Iri string
|
||||||
Inbox string
|
Inbox string
|
||||||
Name sql.NullString
|
SharedInbox sql.NullString
|
||||||
Username string
|
Name sql.NullString
|
||||||
Image sql.NullString
|
Username string
|
||||||
CreatedAt sql.NullTime
|
Image sql.NullString
|
||||||
|
CreatedAt sql.NullTime
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]GetFederationFollowerApprovalRequestsRow, error) {
|
func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]GetFederationFollowerApprovalRequestsRow, error) {
|
||||||
@@ -230,6 +233,7 @@ func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]
|
|||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.Iri,
|
&i.Iri,
|
||||||
&i.Inbox,
|
&i.Inbox,
|
||||||
|
&i.SharedInbox,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.Image,
|
&i.Image,
|
||||||
@@ -249,21 +253,22 @@ func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFederationFollowersWithOffset = `-- name: GetFederationFollowersWithOffset :many
|
const getFederationFollowersWithOffset = `-- name: GetFederationFollowersWithOffset :many
|
||||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2
|
SELECT iri, inbox, shared_inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null ORDER BY created_at DESC LIMIT $1 OFFSET $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetFederationFollowersWithOffsetParams struct {
|
type GetFederationFollowersWithOffsetParams struct {
|
||||||
Limit int
|
Limit int32
|
||||||
Offset int
|
Offset int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetFederationFollowersWithOffsetRow struct {
|
type GetFederationFollowersWithOffsetRow struct {
|
||||||
Iri string
|
Iri string
|
||||||
Inbox string
|
Inbox string
|
||||||
Name sql.NullString
|
SharedInbox sql.NullString
|
||||||
Username string
|
Name sql.NullString
|
||||||
Image sql.NullString
|
Username string
|
||||||
CreatedAt sql.NullTime
|
Image sql.NullString
|
||||||
|
CreatedAt sql.NullTime
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetFederationFollowersWithOffsetParams) ([]GetFederationFollowersWithOffsetRow, error) {
|
func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetFederationFollowersWithOffsetParams) ([]GetFederationFollowersWithOffsetRow, error) {
|
||||||
@@ -278,6 +283,7 @@ func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetF
|
|||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.Iri,
|
&i.Iri,
|
||||||
&i.Inbox,
|
&i.Inbox,
|
||||||
|
&i.SharedInbox,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.Image,
|
&i.Image,
|
||||||
@@ -297,7 +303,7 @@ func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetF
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFollowerByIRI = `-- name: GetFollowerByIRI :one
|
const getFollowerByIRI = `-- name: GetFollowerByIRI :one
|
||||||
SELECT iri, inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1
|
SELECT iri, inbox, shared_inbox, name, username, image, request, request_object, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower, error) {
|
func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower, error) {
|
||||||
@@ -306,6 +312,7 @@ func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower,
|
|||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.Iri,
|
&i.Iri,
|
||||||
&i.Inbox,
|
&i.Inbox,
|
||||||
|
&i.SharedInbox,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.Image,
|
&i.Image,
|
||||||
@@ -365,8 +372,8 @@ SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestam
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetInboundActivitiesWithOffsetParams struct {
|
type GetInboundActivitiesWithOffsetParams struct {
|
||||||
Limit int
|
Limit int32
|
||||||
Offset int
|
Offset int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetInboundActivitiesWithOffsetRow struct {
|
type GetInboundActivitiesWithOffsetRow struct {
|
||||||
@@ -514,8 +521,8 @@ SELECT value FROM ap_outbox LIMIT $1 OFFSET $2
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetOutboxWithOffsetParams struct {
|
type GetOutboxWithOffsetParams struct {
|
||||||
Limit int
|
Limit int32
|
||||||
Offset int
|
Offset int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetOutboxWithOffset(ctx context.Context, arg GetOutboxWithOffsetParams) ([][]byte, error) {
|
func (q *Queries) GetOutboxWithOffset(ctx context.Context, arg GetOutboxWithOffsetParams) ([][]byte, error) {
|
||||||
@@ -584,6 +591,33 @@ func (q *Queries) GetRejectedAndBlockedFollowers(ctx context.Context) ([]GetReje
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUniqueDeliveryInboxes = `-- name: GetUniqueDeliveryInboxes :many
|
||||||
|
SELECT COALESCE(shared_inbox, inbox) as delivery_inbox FROM ap_followers WHERE approved_at is not null GROUP BY delivery_inbox
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUniqueDeliveryInboxes(ctx context.Context) ([]string, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUniqueDeliveryInboxes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []string
|
||||||
|
for rows.Next() {
|
||||||
|
var delivery_inbox string
|
||||||
|
if err := rows.Scan(&delivery_inbox); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, delivery_inbox)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const getUserByAccessToken = `-- name: GetUserByAccessToken :one
|
const getUserByAccessToken = `-- name: GetUserByAccessToken :one
|
||||||
SELECT users.id, display_name, display_color, users.created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes FROM users, user_access_tokens WHERE token = $1 AND users.id = user_id
|
SELECT users.id, display_name, display_color, users.created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes FROM users, user_access_tokens WHERE token = $1 AND users.id = user_id
|
||||||
`
|
`
|
||||||
@@ -758,20 +792,22 @@ func (q *Queries) SetUserAsAuthenticated(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateFollowerByIRI = `-- name: UpdateFollowerByIRI :exec
|
const updateFollowerByIRI = `-- name: UpdateFollowerByIRI :exec
|
||||||
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5
|
UPDATE ap_followers SET inbox = $1, shared_inbox = $2, name = $3, username = $4, image = $5 WHERE iri = $6
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateFollowerByIRIParams struct {
|
type UpdateFollowerByIRIParams struct {
|
||||||
Inbox string
|
Inbox string
|
||||||
Name sql.NullString
|
SharedInbox sql.NullString
|
||||||
Username string
|
Name sql.NullString
|
||||||
Image sql.NullString
|
Username string
|
||||||
Iri string
|
Image sql.NullString
|
||||||
|
Iri string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateFollowerByIRI(ctx context.Context, arg UpdateFollowerByIRIParams) error {
|
func (q *Queries) UpdateFollowerByIRI(ctx context.Context, arg UpdateFollowerByIRIParams) error {
|
||||||
_, err := q.db.ExecContext(ctx, updateFollowerByIRI,
|
_, err := q.db.ExecContext(ctx, updateFollowerByIRI,
|
||||||
arg.Inbox,
|
arg.Inbox,
|
||||||
|
arg.SharedInbox,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
arg.Username,
|
arg.Username,
|
||||||
arg.Image,
|
arg.Image,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
CREATE TABLE IF NOT EXISTS ap_followers (
|
CREATE TABLE IF NOT EXISTS ap_followers (
|
||||||
"iri" TEXT NOT NULL,
|
"iri" TEXT NOT NULL,
|
||||||
"inbox" TEXT NOT NULL,
|
"inbox" TEXT NOT NULL,
|
||||||
|
"shared_inbox" TEXT,
|
||||||
"name" TEXT,
|
"name" TEXT,
|
||||||
"username" TEXT NOT NULL,
|
"username" TEXT NOT NULL,
|
||||||
"image" TEXT,
|
"image" TEXT,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ require (
|
|||||||
github.com/go-fed/httpsig v1.1.0
|
github.com/go-fed/httpsig v1.1.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/grafov/m3u8 v0.12.1
|
github.com/grafov/m3u8 v0.12.1
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
github.com/mattn/go-sqlite3 v1.14.33
|
github.com/mattn/go-sqlite3 v1.14.33
|
||||||
@@ -55,7 +56,6 @@ require (
|
|||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
@@ -79,4 +79,4 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/go-fed/activity => github.com/owncast/activity v1.0.1-0.20260121052038-3b267f104359
|
replace github.com/go-fed/activity => github.com/owncast/activity v1.0.1-0.20260122170223-675f6eb53e71
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||||
@@ -54,6 +56,8 @@ github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s=
|
|||||||
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||||
@@ -83,6 +87,10 @@ github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYr
|
|||||||
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
|
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
@@ -102,8 +110,8 @@ github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNs
|
|||||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||||
github.com/owncast/activity v1.0.1-0.20260121052038-3b267f104359 h1:APHNdGSl1tpxnjT0t+qWhWLhp0zdfIQYKHHBimLQiRY=
|
github.com/owncast/activity v1.0.1-0.20260122170223-675f6eb53e71 h1:dJ6DdVqcZjz0P7UIKPCARucmyIGNXiL7BxNfg1Ul8Gs=
|
||||||
github.com/owncast/activity v1.0.1-0.20260121052038-3b267f104359/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
github.com/owncast/activity v1.0.1-0.20260122170223-675f6eb53e71/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type Follower struct {
|
|||||||
ActorIRI string `json:"link"`
|
ActorIRI string `json:"link"`
|
||||||
// Inbox is the inbox URL of the remote follower
|
// Inbox is the inbox URL of the remote follower
|
||||||
Inbox string `json:"-"`
|
Inbox string `json:"-"`
|
||||||
|
// SharedInbox is the shared inbox URL of the remote follower's server (optional)
|
||||||
|
SharedInbox string `json:"-"`
|
||||||
// Name is the display name of the follower.
|
// Name is the display name of the follower.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// Username is the account username of the remote actor.
|
// Username is the account username of the remote actor.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ func MigrateDatabaseSchema(db *sql.DB, from, to int) error {
|
|||||||
migrateToSchema6(db)
|
migrateToSchema6(db)
|
||||||
case 6:
|
case 6:
|
||||||
migrateToSchema7(db)
|
migrateToSchema7(db)
|
||||||
|
case 7:
|
||||||
|
migrateToSchema8(db)
|
||||||
default:
|
default:
|
||||||
log.Fatalln("missing database migration step")
|
log.Fatalln("missing database migration step")
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,23 @@ func MigrateDatabaseSchema(db *sql.DB, from, to int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateToSchema8(db *sql.DB) {
|
||||||
|
// Add shared_inbox column to ap_followers for ActivityPub shared inbox support.
|
||||||
|
// This allows delivering messages to a server's shared inbox instead of each
|
||||||
|
// user's individual inbox, reducing the number of outbound requests.
|
||||||
|
stmt, err := db.Prepare("ALTER TABLE ap_followers ADD COLUMN shared_inbox TEXT")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("Error running migration. This may be because you have already been running a dev version.", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err = stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func migrateToSchema7(db *sql.DB) {
|
func migrateToSchema7(db *sql.DB) {
|
||||||
log.Println("Migrating users. This may take time if you have lots of users...")
|
log.Println("Migrating users. This may take time if you have lots of users...")
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error {
|
|||||||
defer r.datastore.DbLock.Unlock()
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
||||||
DisplayColor: color,
|
DisplayColor: utils.SafeIntToInt32(color),
|
||||||
ID: userID,
|
ID: userID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return errors.Wrap(err, "unable to change display color")
|
return errors.Wrap(err, "unable to change display color")
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ OWNCAST_HOSTNAME="owncast.local"
|
|||||||
ADMIN_USER="admin"
|
ADMIN_USER="admin"
|
||||||
ADMIN_PASS="abc123"
|
ADMIN_PASS="abc123"
|
||||||
FEDERATION_USERNAME="streamer"
|
FEDERATION_USERNAME="streamer"
|
||||||
|
CLEAR_SHARED_INBOX_PERCENT="${CLEAR_SHARED_INBOX_PERCENT:-30}" # Percentage of followers to clear shared_inbox (0 = none)
|
||||||
|
|
||||||
# URLs (HTTPS via proxy)
|
# URLs (HTTPS via proxy)
|
||||||
SNAC_URL="https://${SNAC_HOSTNAME}:${PROXY_PORT}"
|
SNAC_URL="https://${SNAC_HOSTNAME}:${PROXY_PORT}"
|
||||||
@@ -59,13 +60,13 @@ SNAC_USERNAMES=()
|
|||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; }
|
log_test() { echo -e "${CYAN}[TEST]${NC} $1"; }
|
||||||
|
|
||||||
kill_leftover_processes() {
|
kill_leftover_processes() {
|
||||||
# Kill any leftover test processes from previous runs
|
# Kill any leftover test processes from previous runs
|
||||||
@@ -213,6 +214,44 @@ init_snac2() {
|
|||||||
log_info "snac2 initialized"
|
log_info "snac2 initialized"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enable_snac_shared_inbox() {
|
||||||
|
log_info "Enabling shared_inboxes in snac2 configuration..."
|
||||||
|
|
||||||
|
local server_json="${SNAC_DATA_DIR}/server.json"
|
||||||
|
|
||||||
|
if [[ ! -f "${server_json}" ]]; then
|
||||||
|
log_error "server.json not found at ${server_json}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use jq if available, otherwise use Python as a fallback
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
local tmp_file
|
||||||
|
tmp_file=$(mktemp)
|
||||||
|
jq '. + {"shared_inboxes": true}' "${server_json}" > "${tmp_file}" && mv "${tmp_file}" "${server_json}"
|
||||||
|
elif command -v python3 &> /dev/null; then
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('${server_json}', 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data['shared_inboxes'] = True
|
||||||
|
with open('${server_json}', 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
"
|
||||||
|
else
|
||||||
|
log_error "Neither jq nor python3 available to modify server.json"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the setting was applied
|
||||||
|
if grep -q '"shared_inboxes"' "${server_json}"; then
|
||||||
|
log_info "shared_inboxes enabled in snac2 configuration"
|
||||||
|
else
|
||||||
|
log_error "Failed to enable shared_inboxes"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
create_snac_users() {
|
create_snac_users() {
|
||||||
log_info "Creating ${USER_COUNT} users in snac2 using snac adduser..."
|
log_info "Creating ${USER_COUNT} users in snac2 using snac adduser..."
|
||||||
|
|
||||||
@@ -263,7 +302,9 @@ start_proxy() {
|
|||||||
export CERT_FILE="${CERT_DIR}/cert.pem"
|
export CERT_FILE="${CERT_DIR}/cert.pem"
|
||||||
export KEY_FILE="${CERT_DIR}/key.pem"
|
export KEY_FILE="${CERT_DIR}/key.pem"
|
||||||
|
|
||||||
caddy run --config "${SCRIPT_DIR}/Caddyfile" --adapter caddyfile &
|
# Redirect caddy output to a log file to reduce test noise
|
||||||
|
local caddy_log="${TEMP_DIR}/caddy.log"
|
||||||
|
caddy run --config "${SCRIPT_DIR}/Caddyfile" --adapter caddyfile > "${caddy_log}" 2>&1 &
|
||||||
PROXY_PID=$!
|
PROXY_PID=$!
|
||||||
|
|
||||||
log_info "Caddy started with PID ${PROXY_PID}"
|
log_info "Caddy started with PID ${PROXY_PID}"
|
||||||
@@ -295,8 +336,11 @@ start_snac2() {
|
|||||||
log_info "Starting snac2 server..."
|
log_info "Starting snac2 server..."
|
||||||
|
|
||||||
# mkcert certificates are trusted system-wide, no special env vars needed
|
# mkcert certificates are trusted system-wide, no special env vars needed
|
||||||
"${SNAC_BIN}" httpd "${SNAC_DATA_DIR}" &
|
# Redirect snac2 output to a log file to reduce test noise
|
||||||
|
local snac_log="${TEMP_DIR}/snac2.log"
|
||||||
|
DEBUG=0 "${SNAC_BIN}" httpd "${SNAC_DATA_DIR}" > "${snac_log}" 2>&1 &
|
||||||
SNAC_PID=$!
|
SNAC_PID=$!
|
||||||
|
log_info "snac2 logs available at: ${snac_log}"
|
||||||
|
|
||||||
log_info "snac2 started with PID ${SNAC_PID}"
|
log_info "snac2 started with PID ${SNAC_PID}"
|
||||||
|
|
||||||
@@ -317,6 +361,32 @@ start_snac2() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify_snac_shared_inbox() {
|
||||||
|
log_info "Verifying snac2 actors have sharedInbox endpoint..."
|
||||||
|
|
||||||
|
# Get the first test user to check their actor object
|
||||||
|
if [[ ${#SNAC_USERNAMES[@]} -eq 0 ]]; then
|
||||||
|
log_warn "No snac2 users created yet, skipping sharedInbox verification"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local test_user="${SNAC_USERNAMES[0]}"
|
||||||
|
local actor_url="${SNAC_URL}/${test_user}"
|
||||||
|
|
||||||
|
# Fetch the actor object and check for sharedInbox
|
||||||
|
local actor_response
|
||||||
|
actor_response=$(curl -s --max-time 10 -H "Accept: application/activity+json" "${actor_url}" 2>&1)
|
||||||
|
|
||||||
|
if echo "${actor_response}" | grep -q '"sharedInbox"'; then
|
||||||
|
log_info "sharedInbox endpoint found in snac2 actor objects"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "sharedInbox endpoint NOT found in snac2 actor objects"
|
||||||
|
log_error "Actor response: ${actor_response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
build_owncast() {
|
build_owncast() {
|
||||||
log_info "Building Owncast..."
|
log_info "Building Owncast..."
|
||||||
|
|
||||||
@@ -567,6 +637,51 @@ check_snac_inboxes() {
|
|||||||
echo "${users_with_messages}"
|
echo "${users_with_messages}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Count followers with shared_inbox set vs those with individual inboxes only
|
||||||
|
count_shared_inbox_followers() {
|
||||||
|
local shared_count
|
||||||
|
shared_count=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers WHERE shared_inbox IS NOT NULL AND shared_inbox != '';" 2>/dev/null || echo "0")
|
||||||
|
echo "${shared_count}"
|
||||||
|
}
|
||||||
|
|
||||||
|
count_individual_inbox_followers() {
|
||||||
|
local individual_count
|
||||||
|
individual_count=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers WHERE shared_inbox IS NULL OR shared_inbox = '';" 2>/dev/null || echo "0")
|
||||||
|
echo "${individual_count}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear shared_inbox from a percentage of followers to test mixed delivery
|
||||||
|
clear_some_shared_inboxes() {
|
||||||
|
local percentage=${1:-50} # Default to 50% of followers
|
||||||
|
|
||||||
|
log_info "Clearing shared_inbox from ${percentage}% of followers to test mixed delivery..."
|
||||||
|
|
||||||
|
# Get total follower count
|
||||||
|
local total_followers
|
||||||
|
total_followers=$(sqlite3 "${OWNCAST_DB}" "SELECT COUNT(*) FROM ap_followers;" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [[ "${total_followers}" -eq 0 ]]; then
|
||||||
|
log_warn "No followers found in database"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculate how many to clear
|
||||||
|
local clear_count=$((total_followers * percentage / 100))
|
||||||
|
|
||||||
|
log_info "Total followers: ${total_followers}, clearing shared_inbox from ${clear_count} followers"
|
||||||
|
|
||||||
|
# Clear shared_inbox from a random subset of followers
|
||||||
|
# SQLite's RANDOM() function helps select random rows
|
||||||
|
sqlite3 "${OWNCAST_DB}" "UPDATE ap_followers SET shared_inbox = NULL WHERE iri IN (SELECT iri FROM ap_followers ORDER BY RANDOM() LIMIT ${clear_count});"
|
||||||
|
|
||||||
|
local shared_remaining
|
||||||
|
shared_remaining=$(count_shared_inbox_followers)
|
||||||
|
local individual_count
|
||||||
|
individual_count=$(count_individual_inbox_followers)
|
||||||
|
|
||||||
|
log_info "After clearing: ${shared_remaining} followers with shared_inbox, ${individual_count} with individual inbox only"
|
||||||
|
}
|
||||||
|
|
||||||
verify_all_followers_received_message() {
|
verify_all_followers_received_message() {
|
||||||
local followers=$1
|
local followers=$1
|
||||||
local max_wait=${2:-60}
|
local max_wait=${2:-60}
|
||||||
@@ -624,12 +739,20 @@ print_results() {
|
|||||||
local delivered=$2
|
local delivered=$2
|
||||||
local delivery_time=$3
|
local delivery_time=$3
|
||||||
|
|
||||||
|
# Get shared inbox vs individual inbox counts
|
||||||
|
local shared_inbox_count
|
||||||
|
shared_inbox_count=$(count_shared_inbox_followers)
|
||||||
|
local individual_inbox_count
|
||||||
|
individual_inbox_count=$(count_individual_inbox_followers)
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo "ActivityPub Federation Test Results"
|
echo "ActivityPub Federation Test Results"
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo "Test Users Created: ${USER_COUNT}"
|
echo "Test Users Created: ${USER_COUNT}"
|
||||||
echo "Followers Registered: ${followers}"
|
echo "Followers Registered: ${followers}"
|
||||||
|
echo " - Shared Inbox: ${shared_inbox_count}"
|
||||||
|
echo " - Individual Inbox: ${individual_inbox_count}"
|
||||||
echo "Messages Delivered: ${delivered}"
|
echo "Messages Delivered: ${delivered}"
|
||||||
if [[ -n "${delivery_time}" ]] && [[ "${delivery_time}" -gt 0 ]]; then
|
if [[ -n "${delivery_time}" ]] && [[ "${delivery_time}" -gt 0 ]]; then
|
||||||
echo "Delivery Time: ${delivery_time}s"
|
echo "Delivery Time: ${delivery_time}s"
|
||||||
@@ -676,6 +799,9 @@ main() {
|
|||||||
log_info "Configuration: ${USER_COUNT} test users"
|
log_info "Configuration: ${USER_COUNT} test users"
|
||||||
log_info "Owncast URL: ${OWNCAST_URL}"
|
log_info "Owncast URL: ${OWNCAST_URL}"
|
||||||
log_info "snac2 URL: ${SNAC_URL}"
|
log_info "snac2 URL: ${SNAC_URL}"
|
||||||
|
if [[ "${CLEAR_SHARED_INBOX_PERCENT}" -gt 0 ]]; then
|
||||||
|
log_info "Mixed delivery test: ${CLEAR_SHARED_INBOX_PERCENT}% of followers will use individual inbox"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -689,9 +815,11 @@ main() {
|
|||||||
install_snac2
|
install_snac2
|
||||||
check_certs
|
check_certs
|
||||||
init_snac2
|
init_snac2
|
||||||
|
enable_snac_shared_inbox
|
||||||
create_snac_users
|
create_snac_users
|
||||||
start_proxy
|
start_proxy
|
||||||
start_snac2
|
start_snac2
|
||||||
|
verify_snac_shared_inbox
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -744,6 +872,15 @@ main() {
|
|||||||
else
|
else
|
||||||
log_info "All ${followers} followers registered"
|
log_info "All ${followers} followers registered"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Optionally clear shared_inbox from some followers to test mixed delivery
|
||||||
|
if [[ "${CLEAR_SHARED_INBOX_PERCENT}" -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo "STEP 3.5: Clear shared_inbox from ${CLEAR_SHARED_INBOX_PERCENT}% of followers"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
clear_some_shared_inboxes "${CLEAR_SHARED_INBOX_PERCENT}"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|||||||
@@ -2,13 +2,34 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allowInternalFederation bool
|
||||||
|
allowInternalFederationOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllowInternalFederation returns true if the OWNCAST_ALLOW_INTERNAL_FEDERATION
|
||||||
|
// environment variable is set to "true". This is used for testing purposes only.
|
||||||
|
func AllowInternalFederation() bool {
|
||||||
|
allowInternalFederationOnce.Do(func() {
|
||||||
|
allowInternalFederation = os.Getenv("OWNCAST_ALLOW_INTERNAL_FEDERATION") == "true"
|
||||||
|
})
|
||||||
|
return allowInternalFederation
|
||||||
|
}
|
||||||
|
|
||||||
// IsHostnameInternal will attempt to determine if the hostname is internal to
|
// IsHostnameInternal will attempt to determine if the hostname is internal to
|
||||||
// this server's network or is the loopback address.
|
// this server's network or is the loopback address.
|
||||||
|
// Returns false if OWNCAST_ALLOW_INTERNAL_FEDERATION is set to "true".
|
||||||
func IsHostnameInternal(hostname string) bool {
|
func IsHostnameInternal(hostname string) bool {
|
||||||
|
// Allow internal federation for testing purposes.
|
||||||
|
if AllowInternalFederation() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// If this is already an IP address don't try to resolve it
|
// If this is already an IP address don't try to resolve it
|
||||||
if ip := net.ParseIP(hostname); ip != nil {
|
if ip := net.ParseIP(hostname); ip != nil {
|
||||||
return isIPAddressInternal(ip)
|
return isIPAddressInternal(ip)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -422,6 +423,17 @@ func IntPercentage(x, total int) int {
|
|||||||
return int(float64(x) / float64(total) * 100)
|
return int(float64(x) / float64(total) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SafeIntToInt32 safely converts an int to int32, clamping to int32 bounds.
|
||||||
|
func SafeIntToInt32(n int) int32 {
|
||||||
|
if n > math.MaxInt32 {
|
||||||
|
return math.MaxInt32
|
||||||
|
}
|
||||||
|
if n < math.MinInt32 {
|
||||||
|
return math.MinInt32
|
||||||
|
}
|
||||||
|
return int32(n)
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeBase64Image decodes a base64 image string into a byte array, returning the extension (including dot) for the content type.
|
// DecodeBase64Image decodes a base64 image string into a byte array, returning the extension (including dot) for the content type.
|
||||||
func DecodeBase64Image(url string) (bytes []byte, extension string, err error) {
|
func DecodeBase64Image(url string) (bytes []byte, extension string, err error) {
|
||||||
s := strings.SplitN(url, ",", 2)
|
s := strings.SplitN(url, ",", 2)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/webhooks"
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
@@ -30,9 +30,11 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
followersRepo := followersrepository.Get()
|
||||||
|
|
||||||
if *approval.Approved {
|
if *approval.Approved {
|
||||||
// Approve a follower
|
// Approve a follower
|
||||||
if err := persistence.ApprovePreviousFollowRequest(*approval.ActorIRI); err != nil {
|
if err := followersRepo.ApprovePreviousRequest(*approval.ActorIRI); err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
|
||||||
followRequest, err := persistence.GetFollower(*approval.ActorIRI)
|
followRequest, err := followersRepo.GetByIRI(*approval.ActorIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
@@ -56,7 +58,7 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remove/block a follower
|
// Remove/block a follower
|
||||||
if err := persistence.BlockOrRejectFollower(*approval.ActorIRI); err != nil {
|
if err := followersRepo.BlockOrReject(*approval.ActorIRI); err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,7 +69,8 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// GetPendingFollowRequests will return a list of pending follow requests.
|
// GetPendingFollowRequests will return a list of pending follow requests.
|
||||||
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
||||||
requests, err := persistence.GetPendingFollowRequests()
|
followersRepo := followersrepository.Get()
|
||||||
|
requests, err := followersRepo.GetPendingFollowRequests()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
@@ -78,7 +81,8 @@ func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
||||||
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) {
|
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) {
|
||||||
rejections, err := persistence.GetBlockedAndRejectedFollowers()
|
followersRepo := followersrepository.Get()
|
||||||
|
rejections, err := followersRepo.GetBlockedAndRejected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||||
webutils "github.com/owncast/owncast/webserver/utils"
|
webutils "github.com/owncast/owncast/webserver/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
|
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
|
||||||
func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) {
|
func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) {
|
||||||
followers, total, err := persistence.GetFederationFollowers(limit, offset)
|
followersRepo := followersrepository.Get()
|
||||||
|
followers, total, err := followersRepo.GetFollowers(limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webutils.WriteSimpleResponse(w, false, "unable to fetch followers")
|
webutils.WriteSimpleResponse(w, false, "unable to fetch followers")
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user