Add a layer of safety around required ActivityPub Actor fields (#4703)
* fix(ap): add safe constructors, getters, and validators to ActivityPub actors to address #4701 * chore: replace nil check with the new Validate() method * chore(test): added test to verify the values extracted from actors * fix: return the specific error type + test for it * fix(ap): add recovery for each AP inbox worker for a worst case scenario * fix(ap): add additional safe accessor methods to other AP entities other than actors * chore(tests): add tests for the new safe accessors * fix(ap): handle empty public keys in AP actors
This commit is contained in:
+124
-40
@@ -38,6 +38,129 @@ type ActivityPubActor struct {
|
|||||||
FullUsername string
|
FullUsername string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrActorMissingRequiredField is returned when an actor is missing a required field.
|
||||||
|
var ErrActorMissingRequiredField = errors.New("actor missing required field")
|
||||||
|
|
||||||
|
// Validate checks that required fields are present on the actor.
|
||||||
|
// Returns an error if ActorIri or Inbox are nil.
|
||||||
|
func (a *ActivityPubActor) Validate() error {
|
||||||
|
if a.ActorIri == nil {
|
||||||
|
return fmt.Errorf("%w: ActorIri is required", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
if a.Inbox == nil {
|
||||||
|
return fmt.Errorf("%w: Inbox is required", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if the actor has all required fields.
|
||||||
|
func (a *ActivityPubActor) IsValid() bool {
|
||||||
|
return a.Validate() == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActorIriString returns the string representation of ActorIri, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) ActorIriString() string {
|
||||||
|
if a.ActorIri == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.ActorIri.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InboxString returns the string representation of Inbox, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) InboxString() string {
|
||||||
|
if a.Inbox == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.Inbox.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageString returns the string representation of Image, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) ImageString() string {
|
||||||
|
if a.Image == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.Image.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FollowRequestIriString returns the string representation of FollowRequestIri, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) FollowRequestIriString() string {
|
||||||
|
if a.FollowRequestIri == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.FollowRequestIri.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActorIriHostname returns the hostname of ActorIri, or empty string if nil.
|
||||||
|
func (a *ActivityPubActor) ActorIriHostname() string {
|
||||||
|
if a.ActorIri == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.ActorIri.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewActivityPubActor creates a new ActivityPubActor with required fields.
|
||||||
|
// Returns an error if actorIri or inbox are nil.
|
||||||
|
func NewActivityPubActor(actorIri, inbox *url.URL) (*ActivityPubActor, error) {
|
||||||
|
if actorIri == nil {
|
||||||
|
return nil, fmt.Errorf("%w: actorIri is required", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
if inbox == nil {
|
||||||
|
return nil, fmt.Errorf("%w: inbox is required", ErrActorMissingRequiredField)
|
||||||
|
}
|
||||||
|
return &ActivityPubActor{
|
||||||
|
ActorIri: actorIri,
|
||||||
|
Inbox: inbox,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewActivityPubActorFromEntity creates a new ActivityPubActor from an external entity
|
||||||
|
// with validation of required fields.
|
||||||
|
func NewActivityPubActorFromEntity(entity ExternalEntity) (*ActivityPubActor, error) {
|
||||||
|
// ActorIri is required (must validate before GetFullUsernameFromExternalEntity which uses it)
|
||||||
|
if entity.GetJSONLDId() == nil || entity.GetJSONLDId().Get() == nil {
|
||||||
|
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{
|
||||||
|
ActorIri: actorIri,
|
||||||
|
Inbox: inbox,
|
||||||
|
Name: name,
|
||||||
|
Username: entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString(),
|
||||||
|
FullUsername: username,
|
||||||
|
W3IDSecurityV1PublicKey: entity.GetW3IDSecurityV1PublicKey(),
|
||||||
|
Image: image,
|
||||||
|
}
|
||||||
|
|
||||||
|
return apActor, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteRequest represents a request for delete.
|
// DeleteRequest represents a request for delete.
|
||||||
type DeleteRequest struct {
|
type DeleteRequest struct {
|
||||||
ActorIri string
|
ActorIri string
|
||||||
@@ -53,45 +176,6 @@ type ExternalEntity interface {
|
|||||||
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeActorFromExernalAPEntity takes a full ActivityPub entity and returns our
|
|
||||||
// internal representation of an actor.
|
|
||||||
func MakeActorFromExernalAPEntity(entity ExternalEntity) (*ActivityPubActor, error) {
|
|
||||||
// Username is required (but not a part of the official ActivityPub spec)
|
|
||||||
if entity.GetActivityStreamsPreferredUsername() == nil || entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString() == "" {
|
|
||||||
return nil, errors.New("remote activitypub entity does not have a preferred username set, rejecting")
|
|
||||||
}
|
|
||||||
username := GetFullUsernameFromExternalEntity(entity)
|
|
||||||
|
|
||||||
// Key is required
|
|
||||||
if entity.GetW3IDSecurityV1PublicKey() == nil {
|
|
||||||
return nil, errors.New("remote activitypub entity does not have a public key set, rejecting")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name is optional
|
|
||||||
var name string
|
|
||||||
if entity.GetActivityStreamsName() != nil && !entity.GetActivityStreamsName().Empty() {
|
|
||||||
name = entity.GetActivityStreamsName().At(0).GetXMLSchemaString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image is optional
|
|
||||||
var image *url.URL
|
|
||||||
if entity.GetActivityStreamsIcon() != nil && !entity.GetActivityStreamsIcon().Empty() && entity.GetActivityStreamsIcon().At(0).GetActivityStreamsImage() != nil {
|
|
||||||
image = entity.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI()
|
|
||||||
}
|
|
||||||
|
|
||||||
apActor := ActivityPubActor{
|
|
||||||
ActorIri: entity.GetJSONLDId().Get(),
|
|
||||||
Inbox: entity.GetActivityStreamsInbox().GetIRI(),
|
|
||||||
Name: name,
|
|
||||||
Username: entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString(),
|
|
||||||
FullUsername: username,
|
|
||||||
W3IDSecurityV1PublicKey: entity.GetW3IDSecurityV1PublicKey(),
|
|
||||||
Image: image,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apActor, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeActorPropertyWithID will return an actor property filled with the provided IRI.
|
// MakeActorPropertyWithID will return an actor property filled with the provided IRI.
|
||||||
func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty {
|
func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty {
|
||||||
actor := streams.NewActivityStreamsActorProperty()
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
@@ -241,7 +325,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
|||||||
// GetFullUsernameFromExternalEntity will return the full username from an
|
// GetFullUsernameFromExternalEntity will return the full username from an
|
||||||
// internal representation of an ExternalEntity. Returns user@host.tld.
|
// internal representation of an ExternalEntity. Returns user@host.tld.
|
||||||
func GetFullUsernameFromExternalEntity(entity ExternalEntity) string {
|
func GetFullUsernameFromExternalEntity(entity ExternalEntity) string {
|
||||||
hostname := entity.GetJSONLDId().GetIRI().Hostname()
|
hostname := GetHostnameFromJSONLDId(entity.GetJSONLDId())
|
||||||
username := entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString()
|
username := entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString()
|
||||||
fullUsername := fmt.Sprintf("%s@%s", username, hostname)
|
fullUsername := fmt.Sprintf("%s@%s", username, hostname)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package apmodels
|
package apmodels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -46,6 +47,8 @@ func makeFakeService() vocab.ActivityStreamsService {
|
|||||||
service.SetActivityStreamsIcon(icon)
|
service.SetActivityStreamsIcon(icon)
|
||||||
|
|
||||||
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||||
|
publicKeyType := streams.NewW3IDSecurityV1PublicKey()
|
||||||
|
publicKeyProperty.AppendW3IDSecurityV1PublicKey(publicKeyType)
|
||||||
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
||||||
|
|
||||||
return service
|
return service
|
||||||
@@ -65,34 +68,6 @@ func TestMain(m *testing.M) {
|
|||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeActorFromExternalAPEntity(t *testing.T) {
|
|
||||||
service := makeFakeService()
|
|
||||||
actor, err := MakeActorFromExernalAPEntity(service)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actor.ActorIri != service.GetJSONLDId().GetIRI() {
|
|
||||||
t.Errorf("actor.ID = %v, want %v", actor.ActorIri, service.GetJSONLDId().GetIRI())
|
|
||||||
}
|
|
||||||
|
|
||||||
if actor.Name != service.GetActivityStreamsName().At(0).GetXMLSchemaString() {
|
|
||||||
t.Errorf("actor.Name = %v, want %v", actor.Name, service.GetActivityStreamsName().At(0).GetXMLSchemaString())
|
|
||||||
}
|
|
||||||
|
|
||||||
if actor.Username != service.GetActivityStreamsPreferredUsername().GetXMLSchemaString() {
|
|
||||||
t.Errorf("actor.Username = %v, want %v", actor.Username, service.GetActivityStreamsPreferredUsername().GetXMLSchemaString())
|
|
||||||
}
|
|
||||||
|
|
||||||
if actor.Inbox != service.GetActivityStreamsInbox().GetIRI() {
|
|
||||||
t.Errorf("actor.Inbox = %v, want %v", actor.Inbox.String(), service.GetActivityStreamsInbox().GetIRI())
|
|
||||||
}
|
|
||||||
|
|
||||||
if actor.Image != service.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().At(0).GetIRI() {
|
|
||||||
t.Errorf("actor.Image = %v, want %v", actor.Image, service.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().At(0).GetIRI())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeActorPropertyWithID(t *testing.T) {
|
func TestMakeActorPropertyWithID(t *testing.T) {
|
||||||
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
||||||
actor := MakeActorPropertyWithID(iri)
|
actor := MakeActorPropertyWithID(iri)
|
||||||
@@ -180,3 +155,463 @@ func TestMakeServiceForAccount(t *testing.T) {
|
|||||||
t.Errorf("actor.URL = %v, want %v", person.GetActivityStreamsUrl().At(0).GetIRI().String(), expectedIRI)
|
t.Errorf("actor.URL = %v, want %v", person.GetActivityStreamsUrl().At(0).GetIRI().String(), expectedIRI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for nil-safe accessor methods
|
||||||
|
|
||||||
|
func TestActorIriStringWithNilValue(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
result := actor.ActorIriString()
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("ActorIriString() with nil ActorIri = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActorIriStringWithValue(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
actor := ActivityPubActor{ActorIri: iri}
|
||||||
|
result := actor.ActorIriString()
|
||||||
|
if result != "https://example.com/user/test" {
|
||||||
|
t.Errorf("ActorIriString() = %v, want %v", result, "https://example.com/user/test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboxStringWithNilValue(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
result := actor.InboxString()
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("InboxString() with nil Inbox = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboxStringWithValue(t *testing.T) {
|
||||||
|
inbox, _ := url.Parse("https://example.com/user/test/inbox")
|
||||||
|
actor := ActivityPubActor{Inbox: inbox}
|
||||||
|
result := actor.InboxString()
|
||||||
|
if result != "https://example.com/user/test/inbox" {
|
||||||
|
t.Errorf("InboxString() = %v, want %v", result, "https://example.com/user/test/inbox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageStringWithNilValue(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
result := actor.ImageString()
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("ImageString() with nil Image = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageStringWithValue(t *testing.T) {
|
||||||
|
image, _ := url.Parse("https://example.com/avatar.png")
|
||||||
|
actor := ActivityPubActor{Image: image}
|
||||||
|
result := actor.ImageString()
|
||||||
|
if result != "https://example.com/avatar.png" {
|
||||||
|
t.Errorf("ImageString() = %v, want %v", result, "https://example.com/avatar.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowRequestIriStringWithNilValue(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
result := actor.FollowRequestIriString()
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("FollowRequestIriString() with nil FollowRequestIri = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowRequestIriStringWithValue(t *testing.T) {
|
||||||
|
followIri, _ := url.Parse("https://example.com/follow/123")
|
||||||
|
actor := ActivityPubActor{FollowRequestIri: followIri}
|
||||||
|
result := actor.FollowRequestIriString()
|
||||||
|
if result != "https://example.com/follow/123" {
|
||||||
|
t.Errorf("FollowRequestIriString() = %v, want %v", result, "https://example.com/follow/123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActorIriHostnameWithNilValue(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
result := actor.ActorIriHostname()
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("ActorIriHostname() with nil ActorIri = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActorIriHostnameWithValue(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
actor := ActivityPubActor{ActorIri: iri}
|
||||||
|
result := actor.ActorIriHostname()
|
||||||
|
if result != "example.com" {
|
||||||
|
t.Errorf("ActorIriHostname() = %v, want %v", result, "example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for Validate() and IsValid() methods
|
||||||
|
|
||||||
|
func TestValidateWithAllNilFields(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
err := actor.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Validate() with all nil fields should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("Validate() error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWithNilActorIri(t *testing.T) {
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
actor := ActivityPubActor{Inbox: inbox}
|
||||||
|
err := actor.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Validate() with nil ActorIri should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("Validate() error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWithNilInbox(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
actor := ActivityPubActor{ActorIri: iri}
|
||||||
|
err := actor.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Validate() with nil Inbox should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("Validate() error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWithRequiredFields(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
actor := ActivityPubActor{ActorIri: iri, Inbox: inbox}
|
||||||
|
err := actor.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Validate() with required fields should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValidWithInvalidActor(t *testing.T) {
|
||||||
|
actor := ActivityPubActor{}
|
||||||
|
if actor.IsValid() {
|
||||||
|
t.Error("IsValid() with invalid actor should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValidWithValidActor(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
actor := ActivityPubActor{ActorIri: iri, Inbox: inbox}
|
||||||
|
if !actor.IsValid() {
|
||||||
|
t.Error("IsValid() with valid actor should return true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for NewActivityPubActor constructor
|
||||||
|
|
||||||
|
func TestNewActivityPubActorWithNilActorIri(t *testing.T) {
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
_, err := NewActivityPubActor(nil, inbox)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActor with nil actorIri should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActor error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorWithNilInbox(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
_, err := NewActivityPubActor(iri, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActor with nil inbox should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActor error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorWithBothNil(t *testing.T) {
|
||||||
|
_, err := NewActivityPubActor(nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActor with both nil should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActor error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorWithValidArgs(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
actor, err := NewActivityPubActor(iri, inbox)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewActivityPubActor with valid args should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if actor == nil {
|
||||||
|
t.Error("NewActivityPubActor with valid args should return non-nil actor")
|
||||||
|
}
|
||||||
|
if actor.ActorIri != iri {
|
||||||
|
t.Errorf("actor.ActorIri = %v, want %v", actor.ActorIri, iri)
|
||||||
|
}
|
||||||
|
if actor.Inbox != inbox {
|
||||||
|
t.Errorf("actor.Inbox = %v, want %v", actor.Inbox, inbox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for NewActivityPubActorFromEntity with invalid entities
|
||||||
|
|
||||||
|
func makeFakeServiceWithoutUsername() vocab.ActivityStreamsService {
|
||||||
|
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
||||||
|
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox")
|
||||||
|
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
id.Set(iri)
|
||||||
|
service.SetJSONLDId(id)
|
||||||
|
|
||||||
|
inboxProp := streams.NewActivityStreamsInboxProperty()
|
||||||
|
inboxProp.SetIRI(inbox)
|
||||||
|
service.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
|
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||||
|
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFakeServiceWithoutPublicKey() vocab.ActivityStreamsService {
|
||||||
|
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
||||||
|
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox")
|
||||||
|
username := "foodawg"
|
||||||
|
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
id.Set(iri)
|
||||||
|
service.SetJSONLDId(id)
|
||||||
|
|
||||||
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
|
preferredUsernameProperty.SetXMLSchemaString(username)
|
||||||
|
service.SetActivityStreamsPreferredUsername(preferredUsernameProperty)
|
||||||
|
|
||||||
|
inboxProp := streams.NewActivityStreamsInboxProperty()
|
||||||
|
inboxProp.SetIRI(inbox)
|
||||||
|
service.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFakeServiceWithEmptyPublicKey() vocab.ActivityStreamsService {
|
||||||
|
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
||||||
|
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox")
|
||||||
|
username := "foodawg"
|
||||||
|
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
id.Set(iri)
|
||||||
|
service.SetJSONLDId(id)
|
||||||
|
|
||||||
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
|
preferredUsernameProperty.SetXMLSchemaString(username)
|
||||||
|
service.SetActivityStreamsPreferredUsername(preferredUsernameProperty)
|
||||||
|
|
||||||
|
inboxProp := streams.NewActivityStreamsInboxProperty()
|
||||||
|
inboxProp.SetIRI(inbox)
|
||||||
|
service.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
|
// Set an empty public key property (Len() == 0)
|
||||||
|
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||||
|
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFakeServiceWithoutId() vocab.ActivityStreamsService {
|
||||||
|
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox")
|
||||||
|
username := "foodawg"
|
||||||
|
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
|
||||||
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
|
preferredUsernameProperty.SetXMLSchemaString(username)
|
||||||
|
service.SetActivityStreamsPreferredUsername(preferredUsernameProperty)
|
||||||
|
|
||||||
|
inboxProp := streams.NewActivityStreamsInboxProperty()
|
||||||
|
inboxProp.SetIRI(inbox)
|
||||||
|
service.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
|
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||||
|
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFakeServiceWithoutInbox() vocab.ActivityStreamsService {
|
||||||
|
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo")
|
||||||
|
username := "foodawg"
|
||||||
|
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
id.Set(iri)
|
||||||
|
service.SetJSONLDId(id)
|
||||||
|
|
||||||
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
|
preferredUsernameProperty.SetXMLSchemaString(username)
|
||||||
|
service.SetActivityStreamsPreferredUsername(preferredUsernameProperty)
|
||||||
|
|
||||||
|
publicKeyProperty := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||||
|
service.SetW3IDSecurityV1PublicKey(publicKeyProperty)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithoutUsername(t *testing.T) {
|
||||||
|
service := makeFakeServiceWithoutUsername()
|
||||||
|
_, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActorFromEntity without username should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithoutPublicKey(t *testing.T) {
|
||||||
|
service := makeFakeServiceWithoutPublicKey()
|
||||||
|
_, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActorFromEntity without public key should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithEmptyPublicKey(t *testing.T) {
|
||||||
|
service := makeFakeServiceWithEmptyPublicKey()
|
||||||
|
_, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActorFromEntity with empty public key should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithoutId(t *testing.T) {
|
||||||
|
service := makeFakeServiceWithoutId()
|
||||||
|
_, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActorFromEntity without ID should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithoutInbox(t *testing.T) {
|
||||||
|
service := makeFakeServiceWithoutInbox()
|
||||||
|
_, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewActivityPubActorFromEntity without inbox should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrActorMissingRequiredField) {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity error = %v, want ErrActorMissingRequiredField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActivityPubActorFromEntityWithValidEntity(t *testing.T) {
|
||||||
|
service := makeFakeService()
|
||||||
|
actor, err := NewActivityPubActorFromEntity(service)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewActivityPubActorFromEntity with valid entity should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if actor == nil {
|
||||||
|
t.Fatal("NewActivityPubActorFromEntity with valid entity should return non-nil actor")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify required fields are non-nil
|
||||||
|
if actor.ActorIri == nil {
|
||||||
|
t.Error("actor.ActorIri should not be nil")
|
||||||
|
}
|
||||||
|
if actor.Inbox == nil {
|
||||||
|
t.Error("actor.Inbox should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify extracted values match the fake service data
|
||||||
|
expectedIri := "https://fake.fediverse.server/user/mrfoo"
|
||||||
|
if actor.ActorIriString() != expectedIri {
|
||||||
|
t.Errorf("actor.ActorIri = %v, want %v", actor.ActorIriString(), expectedIri)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedInbox := "https://fake.fediverse.server/user/mrfoo/inbox"
|
||||||
|
if actor.InboxString() != expectedInbox {
|
||||||
|
t.Errorf("actor.Inbox = %v, want %v", actor.InboxString(), expectedInbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedName := "Mr Foo"
|
||||||
|
if actor.Name != expectedName {
|
||||||
|
t.Errorf("actor.Name = %v, want %v", actor.Name, expectedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedUsername := "foodawg"
|
||||||
|
if actor.Username != expectedUsername {
|
||||||
|
t.Errorf("actor.Username = %v, want %v", actor.Username, expectedUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedImage := "https://fake.fediverse.server/user/mrfoo/avatar.png"
|
||||||
|
if actor.ImageString() != expectedImage {
|
||||||
|
t.Errorf("actor.Image = %v, want %v", actor.ImageString(), expectedImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that safe accessors don't panic on zero-value struct
|
||||||
|
|
||||||
|
func TestSafeAccessorsOnZeroValueStruct(t *testing.T) {
|
||||||
|
var actor ActivityPubActor
|
||||||
|
|
||||||
|
// These should not panic
|
||||||
|
_ = actor.ActorIriString()
|
||||||
|
_ = actor.InboxString()
|
||||||
|
_ = actor.ImageString()
|
||||||
|
_ = actor.FollowRequestIriString()
|
||||||
|
_ = actor.ActorIriHostname()
|
||||||
|
_ = actor.Validate()
|
||||||
|
_ = actor.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that safe accessors work correctly with optional nil fields on otherwise valid actor
|
||||||
|
|
||||||
|
func TestSafeAccessorsWithOptionalNilFields(t *testing.T) {
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
inbox, _ := url.Parse("https://example.com/inbox")
|
||||||
|
actor := ActivityPubActor{
|
||||||
|
ActorIri: iri,
|
||||||
|
Inbox: inbox,
|
||||||
|
// Image and FollowRequestIri are intentionally nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required fields should return values
|
||||||
|
if actor.ActorIriString() != "https://example.com/user/test" {
|
||||||
|
t.Errorf("ActorIriString() = %v, want non-empty", actor.ActorIriString())
|
||||||
|
}
|
||||||
|
if actor.InboxString() != "https://example.com/inbox" {
|
||||||
|
t.Errorf("InboxString() = %v, want non-empty", actor.InboxString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional nil fields should return empty strings without panicking
|
||||||
|
if actor.ImageString() != "" {
|
||||||
|
t.Errorf("ImageString() = %v, want empty string", actor.ImageString())
|
||||||
|
}
|
||||||
|
if actor.FollowRequestIriString() != "" {
|
||||||
|
t.Errorf("FollowRequestIriString() = %v, want empty string", actor.FollowRequestIriString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor should still be valid (only ActorIri and Inbox are required)
|
||||||
|
if !actor.IsValid() {
|
||||||
|
t.Error("Actor with ActorIri and Inbox should be valid even with nil optional fields")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package apmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -118,3 +119,149 @@ func GetLogoType() string {
|
|||||||
}
|
}
|
||||||
return logoType
|
return logoType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrMissingIRI is returned when an IRI cannot be extracted from an ActivityStreams property.
|
||||||
|
var ErrMissingIRI = fmt.Errorf("missing IRI")
|
||||||
|
|
||||||
|
// GetIRIFromActorProperty safely extracts the IRI from an ActivityStreamsActorProperty.
|
||||||
|
// Returns the IRI and nil error on success, or nil and an error if the IRI cannot be extracted.
|
||||||
|
func GetIRIFromActorProperty(actor vocab.ActivityStreamsActorProperty) (*url.URL, error) {
|
||||||
|
if actor == nil || actor.Empty() || actor.Len() == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: actor property is empty or nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
first := actor.At(0)
|
||||||
|
if first == nil {
|
||||||
|
return nil, fmt.Errorf("%w: actor property first element is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
iri := first.GetIRI()
|
||||||
|
if iri == nil {
|
||||||
|
return nil, fmt.Errorf("%w: actor IRI is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
return iri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIRIStringFromActorProperty safely extracts the IRI string from an ActivityStreamsActorProperty.
|
||||||
|
// Returns the IRI string and nil error on success, or empty string and an error if extraction fails.
|
||||||
|
func GetIRIStringFromActorProperty(actor vocab.ActivityStreamsActorProperty) (string, error) {
|
||||||
|
iri, err := GetIRIFromActorProperty(actor)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return iri.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIRIFromObjectProperty safely extracts the IRI from an ActivityStreamsObjectProperty.
|
||||||
|
// Returns the IRI and nil error on success, or nil and an error if the IRI cannot be extracted.
|
||||||
|
func GetIRIFromObjectProperty(object vocab.ActivityStreamsObjectProperty) (*url.URL, error) {
|
||||||
|
if object == nil || object.Len() == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: object property is empty or nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
first := object.At(0)
|
||||||
|
if first == nil {
|
||||||
|
return nil, fmt.Errorf("%w: object property first element is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
iri := first.GetIRI()
|
||||||
|
if iri == nil {
|
||||||
|
return nil, fmt.Errorf("%w: object IRI is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
return iri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIRIStringFromObjectProperty safely extracts the IRI string from an ActivityStreamsObjectProperty.
|
||||||
|
// Returns the IRI string and nil error on success, or empty string and an error if extraction fails.
|
||||||
|
func GetIRIStringFromObjectProperty(object vocab.ActivityStreamsObjectProperty) (string, error) {
|
||||||
|
iri, err := GetIRIFromObjectProperty(object)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return iri.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIRIFromJSONLDIdProperty safely extracts the IRI from a JSONLDIdProperty.
|
||||||
|
// Returns the IRI and nil error on success, or nil and an error if the IRI cannot be extracted.
|
||||||
|
func GetIRIFromJSONLDIdProperty(id vocab.JSONLDIdProperty) (*url.URL, error) {
|
||||||
|
if id == nil {
|
||||||
|
return nil, fmt.Errorf("%w: JSONLD id property is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
iri := id.GetIRI()
|
||||||
|
if iri == nil {
|
||||||
|
return nil, fmt.Errorf("%w: JSONLD id IRI is nil", ErrMissingIRI)
|
||||||
|
}
|
||||||
|
return iri, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIRIStringFromJSONLDIdProperty safely extracts the IRI string from a JSONLDIdProperty.
|
||||||
|
// Returns the IRI string and nil error on success, or empty string and an error if extraction fails.
|
||||||
|
func GetIRIStringFromJSONLDIdProperty(id vocab.JSONLDIdProperty) (string, error) {
|
||||||
|
iri, err := GetIRIFromJSONLDIdProperty(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return iri.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicKeyPem safely extracts the public key PEM from a W3IDSecurityV1PublicKey.
|
||||||
|
// Returns the PEM string and nil error on success, or empty string and an error if extraction fails.
|
||||||
|
func GetPublicKeyPem(publicKey vocab.W3IDSecurityV1PublicKey) (string, error) {
|
||||||
|
if publicKey == nil {
|
||||||
|
return "", fmt.Errorf("public key is nil")
|
||||||
|
}
|
||||||
|
pemProp := publicKey.GetW3IDSecurityV1PublicKeyPem()
|
||||||
|
if pemProp == nil {
|
||||||
|
return "", fmt.Errorf("public key PEM property is nil")
|
||||||
|
}
|
||||||
|
return pemProp.Get(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFirstObjectActivityStreamsPerson safely checks if the first element of an
|
||||||
|
// ActivityStreamsObjectProperty is an ActivityStreamsPerson.
|
||||||
|
// Returns false if the object is nil, empty, or the first element is not a Person.
|
||||||
|
func IsFirstObjectActivityStreamsPerson(object vocab.ActivityStreamsObjectProperty) bool {
|
||||||
|
if object == nil || object.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
first := object.At(0)
|
||||||
|
if first == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return first.IsActivityStreamsPerson()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageFromIcon safely extracts the image URL from an ActivityStreamsIconProperty.
|
||||||
|
// Returns the URL and nil error on success, or nil and nil if the icon is not present or invalid.
|
||||||
|
// This handles the common pattern of icon -> image -> url -> iri.
|
||||||
|
func GetImageFromIcon(icon vocab.ActivityStreamsIconProperty) *url.URL {
|
||||||
|
if icon == nil || icon.Empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
first := icon.At(0)
|
||||||
|
if first == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
image := first.GetActivityStreamsImage()
|
||||||
|
if image == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
urlProp := image.GetActivityStreamsUrl()
|
||||||
|
if urlProp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
begin := urlProp.Begin()
|
||||||
|
if begin == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return begin.GetIRI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostnameFromJSONLDId safely extracts the hostname from a JSONLDIdProperty.
|
||||||
|
// Returns the hostname string, or empty string if extraction fails.
|
||||||
|
func GetHostnameFromJSONLDId(id vocab.JSONLDIdProperty) string {
|
||||||
|
if id == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
iri := id.GetIRI()
|
||||||
|
if iri == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return iri.Hostname()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package apmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetIRIFromActorPropertyWithNil(t *testing.T) {
|
||||||
|
_, err := GetIRIFromActorProperty(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetIRIFromActorProperty(nil) should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrMissingIRI) {
|
||||||
|
t.Errorf("GetIRIFromActorProperty(nil) error = %v, want ErrMissingIRI", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromActorPropertyWithEmpty(t *testing.T) {
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
_, err := GetIRIFromActorProperty(actor)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetIRIFromActorProperty with empty actor should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrMissingIRI) {
|
||||||
|
t.Errorf("GetIRIFromActorProperty error = %v, want ErrMissingIRI", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromActorPropertyWithValidIRI(t *testing.T) {
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
actor.AppendIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIFromActorProperty(actor)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIFromActorProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result.String() != "https://example.com/user/test" {
|
||||||
|
t.Errorf("GetIRIFromActorProperty = %v, want %v", result.String(), "https://example.com/user/test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIStringFromActorPropertyWithValidIRI(t *testing.T) {
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
actor.AppendIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIStringFromActorProperty(actor)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIStringFromActorProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result != "https://example.com/user/test" {
|
||||||
|
t.Errorf("GetIRIStringFromActorProperty = %v, want %v", result, "https://example.com/user/test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromObjectPropertyWithNil(t *testing.T) {
|
||||||
|
_, err := GetIRIFromObjectProperty(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetIRIFromObjectProperty(nil) should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrMissingIRI) {
|
||||||
|
t.Errorf("GetIRIFromObjectProperty(nil) error = %v, want ErrMissingIRI", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromObjectPropertyWithEmpty(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
_, err := GetIRIFromObjectProperty(object)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetIRIFromObjectProperty with empty object should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrMissingIRI) {
|
||||||
|
t.Errorf("GetIRIFromObjectProperty error = %v, want ErrMissingIRI", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromObjectPropertyWithValidIRI(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/post/123")
|
||||||
|
object.AppendIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIFromObjectProperty(object)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIFromObjectProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result.String() != "https://example.com/post/123" {
|
||||||
|
t.Errorf("GetIRIFromObjectProperty = %v, want %v", result.String(), "https://example.com/post/123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIStringFromObjectPropertyWithValidIRI(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/post/123")
|
||||||
|
object.AppendIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIStringFromObjectProperty(object)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIStringFromObjectProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result != "https://example.com/post/123" {
|
||||||
|
t.Errorf("GetIRIStringFromObjectProperty = %v, want %v", result, "https://example.com/post/123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromJSONLDIdPropertyWithNil(t *testing.T) {
|
||||||
|
_, err := GetIRIFromJSONLDIdProperty(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetIRIFromJSONLDIdProperty(nil) should return error")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrMissingIRI) {
|
||||||
|
t.Errorf("GetIRIFromJSONLDIdProperty(nil) error = %v, want ErrMissingIRI", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromJSONLDIdPropertyWithValidIRI(t *testing.T) {
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/activity/456")
|
||||||
|
id.SetIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIFromJSONLDIdProperty(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIFromJSONLDIdProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result.String() != "https://example.com/activity/456" {
|
||||||
|
t.Errorf("GetIRIFromJSONLDIdProperty = %v, want %v", result.String(), "https://example.com/activity/456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIStringFromJSONLDIdPropertyWithValidIRI(t *testing.T) {
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/activity/456")
|
||||||
|
id.SetIRI(iri)
|
||||||
|
|
||||||
|
result, err := GetIRIStringFromJSONLDIdProperty(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIStringFromJSONLDIdProperty with valid IRI should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result != "https://example.com/activity/456" {
|
||||||
|
t.Errorf("GetIRIStringFromJSONLDIdProperty = %v, want %v", result, "https://example.com/activity/456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIRIFromJSONLDIdPropertyWithNoIRI(t *testing.T) {
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
// Set a non-IRI value (using Set instead of SetIRI)
|
||||||
|
iri, _ := url.Parse("https://example.com/activity/456")
|
||||||
|
id.Set(iri)
|
||||||
|
|
||||||
|
// When using Set() the IRI should still be retrievable via GetIRI()
|
||||||
|
result, err := GetIRIFromJSONLDIdProperty(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIRIFromJSONLDIdProperty should work with Set(), got error %v", err)
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
t.Error("GetIRIFromJSONLDIdProperty result should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPublicKeyPemWithNil(t *testing.T) {
|
||||||
|
_, err := GetPublicKeyPem(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetPublicKeyPem(nil) should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPublicKeyPemWithValidKey(t *testing.T) {
|
||||||
|
publicKey := streams.NewW3IDSecurityV1PublicKey()
|
||||||
|
pemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
|
||||||
|
pemProp.Set("-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----")
|
||||||
|
publicKey.SetW3IDSecurityV1PublicKeyPem(pemProp)
|
||||||
|
|
||||||
|
result, err := GetPublicKeyPem(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetPublicKeyPem with valid key should not return error, got %v", err)
|
||||||
|
}
|
||||||
|
if result != "-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----" {
|
||||||
|
t.Errorf("GetPublicKeyPem = %v, want PEM string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPublicKeyPemWithNoPem(t *testing.T) {
|
||||||
|
publicKey := streams.NewW3IDSecurityV1PublicKey()
|
||||||
|
// Don't set PEM property
|
||||||
|
|
||||||
|
_, err := GetPublicKeyPem(publicKey)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetPublicKeyPem with no PEM should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that safe accessors don't panic on nil/empty inputs
|
||||||
|
func TestSafeAccessorsNoPanic(t *testing.T) {
|
||||||
|
// These should not panic
|
||||||
|
_, _ = GetIRIFromActorProperty(nil)
|
||||||
|
_, _ = GetIRIStringFromActorProperty(nil)
|
||||||
|
_, _ = GetIRIFromObjectProperty(nil)
|
||||||
|
_, _ = GetIRIStringFromObjectProperty(nil)
|
||||||
|
_, _ = GetIRIFromJSONLDIdProperty(nil)
|
||||||
|
_, _ = GetIRIStringFromJSONLDIdProperty(nil)
|
||||||
|
_, _ = GetPublicKeyPem(nil)
|
||||||
|
_ = GetImageFromIcon(nil)
|
||||||
|
_ = GetHostnameFromJSONLDId(nil)
|
||||||
|
_ = IsFirstObjectActivityStreamsPerson(nil)
|
||||||
|
|
||||||
|
// Empty properties should also not panic
|
||||||
|
_, _ = GetIRIFromActorProperty(streams.NewActivityStreamsActorProperty())
|
||||||
|
_, _ = GetIRIFromObjectProperty(streams.NewActivityStreamsObjectProperty())
|
||||||
|
_ = GetImageFromIcon(streams.NewActivityStreamsIconProperty())
|
||||||
|
_ = IsFirstObjectActivityStreamsPerson(streams.NewActivityStreamsObjectProperty())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetImageFromIconWithNil(t *testing.T) {
|
||||||
|
result := GetImageFromIcon(nil)
|
||||||
|
if result != nil {
|
||||||
|
t.Error("GetImageFromIcon(nil) should return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetImageFromIconWithEmpty(t *testing.T) {
|
||||||
|
icon := streams.NewActivityStreamsIconProperty()
|
||||||
|
result := GetImageFromIcon(icon)
|
||||||
|
if result != nil {
|
||||||
|
t.Error("GetImageFromIcon with empty icon should return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetImageFromIconWithValidImage(t *testing.T) {
|
||||||
|
icon := streams.NewActivityStreamsIconProperty()
|
||||||
|
image := streams.NewActivityStreamsImage()
|
||||||
|
urlProp := streams.NewActivityStreamsUrlProperty()
|
||||||
|
imageURL, _ := url.Parse("https://example.com/avatar.png")
|
||||||
|
urlProp.AppendIRI(imageURL)
|
||||||
|
image.SetActivityStreamsUrl(urlProp)
|
||||||
|
icon.AppendActivityStreamsImage(image)
|
||||||
|
|
||||||
|
result := GetImageFromIcon(icon)
|
||||||
|
if result == nil {
|
||||||
|
t.Error("GetImageFromIcon with valid image should not return nil")
|
||||||
|
}
|
||||||
|
if result.String() != "https://example.com/avatar.png" {
|
||||||
|
t.Errorf("GetImageFromIcon = %v, want %v", result.String(), "https://example.com/avatar.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHostnameFromJSONLDIdWithNil(t *testing.T) {
|
||||||
|
result := GetHostnameFromJSONLDId(nil)
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("GetHostnameFromJSONLDId(nil) = %v, want empty string", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHostnameFromJSONLDIdWithValidId(t *testing.T) {
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
iri, _ := url.Parse("https://example.com/user/test")
|
||||||
|
id.SetIRI(iri)
|
||||||
|
|
||||||
|
result := GetHostnameFromJSONLDId(id)
|
||||||
|
if result != "example.com" {
|
||||||
|
t.Errorf("GetHostnameFromJSONLDId = %v, want %v", result, "example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFirstObjectActivityStreamsPersonWithNil(t *testing.T) {
|
||||||
|
result := IsFirstObjectActivityStreamsPerson(nil)
|
||||||
|
if result {
|
||||||
|
t.Error("IsFirstObjectActivityStreamsPerson(nil) should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFirstObjectActivityStreamsPersonWithEmpty(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
result := IsFirstObjectActivityStreamsPerson(object)
|
||||||
|
if result {
|
||||||
|
t.Error("IsFirstObjectActivityStreamsPerson with empty object should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFirstObjectActivityStreamsPersonWithPerson(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
person := streams.NewActivityStreamsPerson()
|
||||||
|
object.AppendActivityStreamsPerson(person)
|
||||||
|
|
||||||
|
result := IsFirstObjectActivityStreamsPerson(object)
|
||||||
|
if !result {
|
||||||
|
t.Error("IsFirstObjectActivityStreamsPerson with person should return true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFirstObjectActivityStreamsPersonWithNonPerson(t *testing.T) {
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
note := streams.NewActivityStreamsNote()
|
||||||
|
object.AppendActivityStreamsNote(note)
|
||||||
|
|
||||||
|
result := IsFirstObjectActivityStreamsPerson(object)
|
||||||
|
if result {
|
||||||
|
t.Error("IsFirstObjectActivityStreamsPerson with note should return false")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnnounce) error {
|
func handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnnounce) error {
|
||||||
object := activity.GetActivityStreamsObject()
|
objectIRI, err := apmodels.GetIRIStringFromObjectProperty(activity.GetActivityStreamsObject())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "announce activity is missing object IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorIRI, err := apmodels.GetIRIStringFromActorProperty(activity.GetActivityStreamsActor())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "announce activity is missing actor IRI")
|
||||||
|
}
|
||||||
|
|
||||||
actorReference := activity.GetActivityStreamsActor()
|
actorReference := activity.GetActivityStreamsActor()
|
||||||
objectIRI := object.At(0).GetIRI().String()
|
|
||||||
actorIRI := actorReference.At(0).GetIRI().String()
|
|
||||||
|
|
||||||
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementRepost); hasPreviouslyhandled || err != nil {
|
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementRepost); hasPreviouslyhandled || err != nil {
|
||||||
return errors.Wrap(err, "inbound activity of share/re-post has already been handled")
|
return errors.Wrap(err, "inbound activity of share/re-post has already been handled")
|
||||||
|
|||||||
@@ -24,14 +24,17 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get actor of the action
|
// Get actor of the action
|
||||||
actor, _ := resolvers.GetResolvedActorFromActorProperty(actorReference)
|
actor, err := resolvers.GetResolvedActorFromActorProperty(actorReference)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to resolve actor for engagement activity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Send chat message
|
// Send chat message
|
||||||
actorName := actor.Name
|
actorName := actor.Name
|
||||||
if actorName == "" {
|
if actorName == "" {
|
||||||
actorName = actor.Username
|
actorName = actor.Username
|
||||||
}
|
}
|
||||||
actorIRI := actorReference.Begin().GetIRI().String()
|
actorIRI := actor.ActorIriString()
|
||||||
|
|
||||||
userPrefix := fmt.Sprintf("%s ", actorName)
|
userPrefix := fmt.Sprintf("%s ", actorName)
|
||||||
var suffix string
|
var suffix string
|
||||||
@@ -51,9 +54,8 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
|||||||
body := fmt.Sprintf("%s %s", userPrefix, suffix)
|
body := fmt.Sprintf("%s %s", userPrefix, suffix)
|
||||||
|
|
||||||
var image *string
|
var image *string
|
||||||
if actor.Image != nil {
|
if imageStr := actor.ImageString(); imageStr != "" {
|
||||||
s := actor.Image.String()
|
image = &imageStr
|
||||||
image = &s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chat.SendFediverseAction(eventType, actor.FullUsername, image, body, actorIRI); err != nil {
|
if err := chat.SendFediverseAction(eventType, actor.FullUsername, image, body, actorIRI); err != nil {
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCreateRequest(c context.Context, activity vocab.ActivityStreamsCreate) error {
|
func handleCreateRequest(c context.Context, activity vocab.ActivityStreamsCreate) error {
|
||||||
iri := activity.GetJSONLDId().GetIRI().String()
|
iri, err := apmodels.GetIRIStringFromJSONLDIdProperty(activity.GetJSONLDId())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "create activity is missing IRI")
|
||||||
|
}
|
||||||
return errors.New("not handling create request of: " + iri)
|
return errors.New("not handling create request of: " + iri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
@@ -40,10 +41,18 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
|||||||
}
|
}
|
||||||
|
|
||||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
|
||||||
|
objectIRI, err := apmodels.GetIRIStringFromObjectProperty(activity.GetActivityStreamsObject())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "follow activity is missing object IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorIRI, err := apmodels.GetIRIStringFromActorProperty(activity.GetActivityStreamsActor())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "follow activity is missing actor IRI")
|
||||||
|
}
|
||||||
|
|
||||||
actorReference := activity.GetActivityStreamsActor()
|
actorReference := activity.GetActivityStreamsActor()
|
||||||
object := activity.GetActivityStreamsObject()
|
|
||||||
objectIRI := object.At(0).GetIRI().String()
|
|
||||||
actorIRI := actorReference.At(0).GetIRI().String()
|
|
||||||
|
|
||||||
if approved {
|
if approved {
|
||||||
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
||||||
|
|||||||
+11
-16
@@ -5,30 +5,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error {
|
func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error {
|
||||||
object := activity.GetActivityStreamsObject()
|
objectIRI, err := apmodels.GetIRIStringFromObjectProperty(activity.GetActivityStreamsObject())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "like activity is missing object IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorIRI, err := apmodels.GetIRIStringFromActorProperty(activity.GetActivityStreamsActor())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "like activity is missing actor IRI")
|
||||||
|
}
|
||||||
|
|
||||||
actorReference := activity.GetActivityStreamsActor()
|
actorReference := activity.GetActivityStreamsActor()
|
||||||
|
|
||||||
if object.Len() < 1 {
|
|
||||||
return errors.New("like activity is missing object")
|
|
||||||
}
|
|
||||||
|
|
||||||
if actorReference.Len() < 1 {
|
|
||||||
return errors.New("like activity is missing actor")
|
|
||||||
}
|
|
||||||
|
|
||||||
if object.At(0).GetIRI() == nil {
|
|
||||||
return errors.New("like activity iri is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
objectIRI := object.At(0).GetIRI().String()
|
|
||||||
actorIRI := actorReference.At(0).GetIRI().String()
|
|
||||||
|
|
||||||
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementLike); hasPreviouslyhandled || err != nil {
|
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementLike); hasPreviouslyhandled || err != nil {
|
||||||
return errors.Wrap(err, "inbound activity of like has already been handled")
|
return errors.Wrap(err, "inbound activity of like has already been handled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package inbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseURL(s string) *url.URL {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// These tests verify that handler functions don't panic when given
|
||||||
|
// ActivityPub activities with nil or missing properties that could
|
||||||
|
// cause nil pointer dereferences.
|
||||||
|
|
||||||
|
func TestHandleFollowWithNilObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsFollow()
|
||||||
|
// Don't set object or actor - they will be nil
|
||||||
|
|
||||||
|
// This should return an error, not panic
|
||||||
|
err := handleFollowInboxRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleFollowInboxRequest with nil object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleFollowWithEmptyObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsFollow()
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
// Object is set but empty (no items)
|
||||||
|
|
||||||
|
err := handleFollowInboxRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleFollowInboxRequest with empty object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleFollowWithNilActorIRI(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsFollow()
|
||||||
|
|
||||||
|
// Set a valid object with IRI
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
objectNote := streams.NewActivityStreamsNote()
|
||||||
|
objectID := streams.NewJSONLDIdProperty()
|
||||||
|
objectID.SetIRI(mustParseURL("https://example.com/note/1"))
|
||||||
|
objectNote.SetJSONLDId(objectID)
|
||||||
|
object.AppendActivityStreamsNote(objectNote)
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
// Actor is nil
|
||||||
|
err := handleFollowInboxRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleFollowInboxRequest with nil actor should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAnnounceWithNilObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsAnnounce()
|
||||||
|
// Don't set object or actor
|
||||||
|
|
||||||
|
err := handleAnnounceRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleAnnounceRequest with nil object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAnnounceWithEmptyObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsAnnounce()
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
err := handleAnnounceRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleAnnounceRequest with empty object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAnnounceWithNilActorIRI(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsAnnounce()
|
||||||
|
|
||||||
|
// Set object with IRI
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
object.AppendIRI(mustParseURL("https://example.com/note/1"))
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
// Actor is nil
|
||||||
|
err := handleAnnounceRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleAnnounceRequest with nil actor should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLikeWithNilObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsLike()
|
||||||
|
// Don't set object or actor
|
||||||
|
|
||||||
|
err := handleLikeRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleLikeRequest with nil object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLikeWithEmptyObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsLike()
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
err := handleLikeRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleLikeRequest with empty object should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLikeWithNilActorIRI(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsLike()
|
||||||
|
|
||||||
|
// Set object with IRI
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
object.AppendIRI(mustParseURL("https://example.com/note/1"))
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
// Actor is nil
|
||||||
|
err := handleLikeRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleLikeRequest with nil actor should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleCreateWithNilId(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsCreate()
|
||||||
|
// Don't set JSONLD ID
|
||||||
|
|
||||||
|
err := handleCreateRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleCreateRequest with nil ID should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleCreateWithIdButNilIRI(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsCreate()
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
// Set the ID property but don't set an IRI on it
|
||||||
|
activity.SetJSONLDId(id)
|
||||||
|
|
||||||
|
err := handleCreateRequest(context.Background(), activity)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("handleCreateRequest with ID but nil IRI should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUpdateWithNilObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsUpdate()
|
||||||
|
// Don't set object - should return nil (not an error, just skip)
|
||||||
|
|
||||||
|
// This should not panic and should return nil since we only care about Person updates
|
||||||
|
err := handleUpdateRequest(context.Background(), activity)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("handleUpdateRequest with nil object should return nil (skip), got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUpdateWithEmptyObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsUpdate()
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
// Should return nil since empty object means it's not a Person update
|
||||||
|
err := handleUpdateRequest(context.Background(), activity)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("handleUpdateRequest with empty object should return nil (skip), got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUpdateWithNonPersonObject(t *testing.T) {
|
||||||
|
activity := streams.NewActivityStreamsUpdate()
|
||||||
|
object := streams.NewActivityStreamsObjectProperty()
|
||||||
|
note := streams.NewActivityStreamsNote()
|
||||||
|
object.AppendActivityStreamsNote(note)
|
||||||
|
activity.SetActivityStreamsObject(object)
|
||||||
|
|
||||||
|
// Should return nil since it's not a Person update
|
||||||
|
err := handleUpdateRequest(context.Background(), activity)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("handleUpdateRequest with non-Person object should return nil (skip), got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNilSafetyNoPanic verifies that none of the handlers panic when given
|
||||||
|
// completely empty activities. This is the most important test - we want to
|
||||||
|
// ensure that malformed ActivityPub payloads don't crash the server.
|
||||||
|
func TestNilSafetyNoPanic(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("Follow with nil properties", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("handleFollowInboxRequest panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
activity := streams.NewActivityStreamsFollow()
|
||||||
|
_ = handleFollowInboxRequest(ctx, activity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Announce with nil properties", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("handleAnnounceRequest panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
activity := streams.NewActivityStreamsAnnounce()
|
||||||
|
_ = handleAnnounceRequest(ctx, activity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Like with nil properties", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("handleLikeRequest panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
activity := streams.NewActivityStreamsLike()
|
||||||
|
_ = handleLikeRequest(ctx, activity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Create with nil properties", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("handleCreateRequest panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
activity := streams.NewActivityStreamsCreate()
|
||||||
|
_ = handleCreateRequest(ctx, activity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update with nil properties", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("handleUpdateRequest panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
activity := streams.NewActivityStreamsUpdate()
|
||||||
|
_ = handleUpdateRequest(ctx, activity)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"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/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error {
|
func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error {
|
||||||
// We only care about update events to followers.
|
// We only care about update events to followers.
|
||||||
if !activity.GetActivityStreamsObject().At(0).IsActivityStreamsPerson() {
|
if !apmodels.IsFirstObjectActivityStreamsPerson(activity.GetActivityStreamsObject()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,5 +22,5 @@ func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistence.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String())
|
return persistence.UpdateFollower(actor.ActorIriString(), actor.InboxString(), actor.Name, actor.FullUsername, actor.ImageString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ func Verify(request *http.Request) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
key := publicKey.GetW3IDSecurityV1PublicKeyPem().Get()
|
key, err := apmodels.GetPublicKeyPem(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "failed to get public key PEM")
|
||||||
|
}
|
||||||
block, _ := pem.Decode([]byte(key))
|
block, _ := pem.Decode([]byte(key))
|
||||||
if block == nil {
|
if block == nil {
|
||||||
log.Errorln("failed to parse PEM block containing the public key")
|
log.Errorln("failed to parse PEM block containing the public key")
|
||||||
|
|||||||
@@ -37,7 +37,14 @@ func worker(workerID int, queue <-chan Job) {
|
|||||||
log.Debugf("Started ActivityPub worker %d", workerID)
|
log.Debugf("Started ActivityPub worker %d", workerID)
|
||||||
|
|
||||||
for job := range queue {
|
for job := range queue {
|
||||||
handle(job.request)
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Errorf("Recovered from panic in ActivityPub worker %d: %v", workerID, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
handle(job.request)
|
||||||
|
}()
|
||||||
|
|
||||||
log.Tracef("Done with ActivityPub inbox handler using worker %d", workerID)
|
log.Tracef("Done with ActivityPub inbox handler using worker %d", workerID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAddWithNilId verifies that Add doesn't panic when given an item with nil JSONLD ID.
|
||||||
|
func TestAddWithNilId(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Add panicked with nil ID: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
note := streams.NewActivityStreamsNote()
|
||||||
|
// Don't set JSONLD ID
|
||||||
|
|
||||||
|
err := Add(note, "test-id", false)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Add with nil ID should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddWithIdButNilIRI verifies that Add doesn't panic when given an item
|
||||||
|
// with a JSONLD ID property that has no IRI set.
|
||||||
|
func TestAddWithIdButNilIRI(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Add panicked with ID but nil IRI: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
note := streams.NewActivityStreamsNote()
|
||||||
|
id := streams.NewJSONLDIdProperty()
|
||||||
|
// Set the ID property but don't set an IRI on it
|
||||||
|
note.SetJSONLDId(id)
|
||||||
|
|
||||||
|
err := Add(note, "test-id", false)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Add with ID but nil IRI should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddNoPanic verifies that Add doesn't panic with various nil/empty inputs.
|
||||||
|
func TestAddNoPanic(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
item func() vocab.ActivityStreamsNote
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "note with no properties",
|
||||||
|
item: func() vocab.ActivityStreamsNote {
|
||||||
|
return streams.NewActivityStreamsNote()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "note with empty ID property",
|
||||||
|
item: func() vocab.ActivityStreamsNote {
|
||||||
|
note := streams.NewActivityStreamsNote()
|
||||||
|
note.SetJSONLDId(streams.NewJSONLDIdProperty())
|
||||||
|
return note
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Add panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = Add(tc.item(), "test-id", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -308,13 +308,12 @@ func UpdateFollowersWithAccountUpdates() error {
|
|||||||
|
|
||||||
// Add will save an ActivityPub object to the datastore.
|
// Add will save an ActivityPub object to the datastore.
|
||||||
func Add(item vocab.Type, id string, isLiveNotification bool) error {
|
func Add(item vocab.Type, id string, isLiveNotification bool) error {
|
||||||
iri := item.GetJSONLDId().GetIRI().String()
|
iri, err := apmodels.GetIRIStringFromJSONLDIdProperty(item.GetJSONLDId())
|
||||||
typeString := item.GetTypeName()
|
if err != nil {
|
||||||
|
log.Errorln("Unable to get iri from item:", err)
|
||||||
if iri == "" {
|
return errors.Wrap(err, "unable to get iri from item "+id)
|
||||||
log.Errorln("Unable to get iri from item")
|
|
||||||
return errors.New("Unable to get iri from item " + id)
|
|
||||||
}
|
}
|
||||||
|
typeString := item.GetTypeName()
|
||||||
|
|
||||||
b, err := apmodels.Serialize(item)
|
b, err := apmodels.Serialize(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,23 +33,26 @@ func Setup(datastore *data.Datastore) {
|
|||||||
|
|
||||||
// AddFollow will save a follow to the datastore.
|
// AddFollow will save a follow to the datastore.
|
||||||
func AddFollow(follow apmodels.ActivityPubActor, approved bool) error {
|
func AddFollow(follow apmodels.ActivityPubActor, approved bool) error {
|
||||||
log.Traceln("Saving", follow.ActorIri, "as a follower.")
|
if err := follow.Validate(); err != nil {
|
||||||
var image string
|
return errors.Wrap(err, "cannot add invalid follow")
|
||||||
if follow.Image != nil {
|
|
||||||
image = follow.Image.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Traceln("Saving", follow.ActorIriString(), "as a follower.")
|
||||||
|
|
||||||
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error serializing follow request object")
|
return errors.Wrap(err, "error serializing follow request object")
|
||||||
}
|
}
|
||||||
|
|
||||||
return createFollow(follow.ActorIri.String(), follow.Inbox.String(), follow.FollowRequestIri.String(), follow.Name, follow.Username, image, followRequestObject, approved)
|
return createFollow(follow.ActorIriString(), follow.InboxString(), follow.FollowRequestIriString(), follow.Name, follow.Username, follow.ImageString(), followRequestObject, approved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFollow will remove a follow from the datastore.
|
// RemoveFollow will remove a follow from the datastore.
|
||||||
func RemoveFollow(unfollow apmodels.ActivityPubActor) error {
|
func RemoveFollow(unfollow apmodels.ActivityPubActor) error {
|
||||||
log.Traceln("Removing", unfollow.ActorIri, "as a follower.")
|
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)
|
return removeFollow(unfollow.ActorIri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ func getPersonFromFollow(activity vocab.ActivityStreamsFollow) (apmodels.Activit
|
|||||||
func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) (*apmodels.ActivityPubActor, error) {
|
func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) (*apmodels.ActivityPubActor, error) {
|
||||||
person, err := getPersonFromFollow(activity)
|
person, err := getPersonFromFollow(activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("unable to resolve person from follow request: " + err.Error())
|
return nil, errors.Wrap(err, "unable to resolve person from follow request")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := person.ActorIri.Hostname()
|
hostname := person.ActorIriHostname()
|
||||||
username := person.Username
|
username := person.Username
|
||||||
fullUsername := fmt.Sprintf("%s@%s", username, hostname)
|
fullUsername := fmt.Sprintf("%s@%s", username, hostname)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseURL(s string) *url.URL {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithNil verifies that the function
|
||||||
|
// doesn't panic when given a nil actor property.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithNil(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("GetResolvedActorFromActorProperty panicked with nil: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err := GetResolvedActorFromActorProperty(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetResolvedActorFromActorProperty(nil) should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithEmpty verifies that the function
|
||||||
|
// doesn't panic when given an empty actor property.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithEmpty(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("GetResolvedActorFromActorProperty panicked with empty: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
_, err := GetResolvedActorFromActorProperty(actor)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetResolvedActorFromActorProperty with empty actor should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithIRI verifies that the function
|
||||||
|
// handles the case where an IRI needs to be resolved.
|
||||||
|
// Note: This test involves network calls and config repository access which may
|
||||||
|
// panic in a test environment. This is expected since we're testing nil safety
|
||||||
|
// of ActivityPub property handling, not network behavior.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithIRI(t *testing.T) {
|
||||||
|
t.Skip("Skipping IRI resolution test - requires network access and proper config setup")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithPersonButMissingFields verifies that
|
||||||
|
// the function handles Person objects that are missing required fields.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithPersonMissingFields(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("GetResolvedActorFromActorProperty panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
person := streams.NewActivityStreamsPerson()
|
||||||
|
// Person has no ID, inbox, username, or public key
|
||||||
|
actor.AppendActivityStreamsPerson(person)
|
||||||
|
|
||||||
|
_, err := GetResolvedActorFromActorProperty(actor)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetResolvedActorFromActorProperty with incomplete Person should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithServiceMissingFields verifies that
|
||||||
|
// the function handles Service objects that are missing required fields.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithServiceMissingFields(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("GetResolvedActorFromActorProperty panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
service := streams.NewActivityStreamsService()
|
||||||
|
// Service has no ID, inbox, username, or public key
|
||||||
|
actor.AppendActivityStreamsService(service)
|
||||||
|
|
||||||
|
_, err := GetResolvedActorFromActorProperty(actor)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetResolvedActorFromActorProperty with incomplete Service should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetResolvedActorFromActorPropertyWithApplicationMissingFields verifies that
|
||||||
|
// the function handles Application objects that are missing required fields.
|
||||||
|
func TestGetResolvedActorFromActorPropertyWithApplicationMissingFields(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("GetResolvedActorFromActorProperty panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
app := streams.NewActivityStreamsApplication()
|
||||||
|
// Application has no ID, inbox, username, or public key
|
||||||
|
actor.AppendActivityStreamsApplication(app)
|
||||||
|
|
||||||
|
_, err := GetResolvedActorFromActorProperty(actor)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetResolvedActorFromActorProperty with incomplete Application should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNilSafetyNoPanic is a comprehensive test that ensures none of the
|
||||||
|
// resolver functions panic with nil/empty inputs.
|
||||||
|
func TestNilSafetyNoPanic(t *testing.T) {
|
||||||
|
t.Run("GetResolvedActorFromActorProperty with nil", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, _ = GetResolvedActorFromActorProperty(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetResolvedActorFromActorProperty with empty", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, _ = GetResolvedActorFromActorProperty(streams.NewActivityStreamsActorProperty())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetResolvedActorFromActorProperty with Person no fields", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
actor.AppendActivityStreamsPerson(streams.NewActivityStreamsPerson())
|
||||||
|
_, _ = GetResolvedActorFromActorProperty(actor)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetResolvedActorFromActorProperty with Service no fields", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
actor.AppendActivityStreamsService(streams.NewActivityStreamsService())
|
||||||
|
_, _ = GetResolvedActorFromActorProperty(actor)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetResolvedActorFromActorProperty with Application no fields", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
actor := streams.NewActivityStreamsActorProperty()
|
||||||
|
actor.AppendActivityStreamsApplication(streams.NewActivityStreamsApplication())
|
||||||
|
_, _ = GetResolvedActorFromActorProperty(actor)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -75,52 +75,45 @@ func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
|
|||||||
|
|
||||||
// GetResolvedActorFromActorProperty resolve an external actor property to a
|
// GetResolvedActorFromActorProperty resolve an external actor property to a
|
||||||
// fully populated internal actor representation.
|
// fully populated internal actor representation.
|
||||||
|
// Returns an error if the actor cannot be resolved or is missing required fields.
|
||||||
func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) {
|
func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) {
|
||||||
var err error
|
|
||||||
var apActor apmodels.ActivityPubActor
|
var apActor apmodels.ActivityPubActor
|
||||||
resolved := false
|
|
||||||
|
|
||||||
if !actor.Empty() && actor.Len() > 0 && actor.At(0) != nil {
|
if actor == nil || actor.Empty() || actor.Len() == 0 || actor.At(0) == nil {
|
||||||
// Explicitly use only the first actor that might be listed.
|
return apActor, errors.New("actor property is empty or nil")
|
||||||
actorObjectOrIRI := actor.At(0)
|
|
||||||
var actorEntity apmodels.ExternalEntity
|
|
||||||
|
|
||||||
// If the actor is an unresolved IRI then we need to resolve it.
|
|
||||||
if actorObjectOrIRI.IsIRI() {
|
|
||||||
iri := actorObjectOrIRI.GetIRI().String()
|
|
||||||
return GetResolvedActorFromIRI(iri)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actorObjectOrIRI.IsActivityStreamsPerson() {
|
|
||||||
actorEntity = actorObjectOrIRI.GetActivityStreamsPerson()
|
|
||||||
} else if actorObjectOrIRI.IsActivityStreamsService() {
|
|
||||||
actorEntity = actorObjectOrIRI.GetActivityStreamsService()
|
|
||||||
} else if actorObjectOrIRI.IsActivityStreamsApplication() {
|
|
||||||
actorEntity = actorObjectOrIRI.GetActivityStreamsApplication()
|
|
||||||
} else {
|
|
||||||
err = errors.New("unrecognized external ActivityPub type: " + actorObjectOrIRI.Name())
|
|
||||||
return apActor, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any of the resolution or population failed then return the error.
|
|
||||||
if err != nil {
|
|
||||||
return apActor, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the external AP entity into an internal actor representation.
|
|
||||||
apa, e := apmodels.MakeActorFromExernalAPEntity(actorEntity)
|
|
||||||
if apa != nil {
|
|
||||||
apActor = *apa
|
|
||||||
resolved = true
|
|
||||||
}
|
|
||||||
err = e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resolved && err == nil {
|
// Explicitly use only the first actor that might be listed.
|
||||||
err = errors.New("unknown error resolving actor from property value")
|
actorObjectOrIRI := actor.At(0)
|
||||||
|
var actorEntity apmodels.ExternalEntity
|
||||||
|
|
||||||
|
// If the actor is an unresolved IRI then we need to resolve it.
|
||||||
|
if actorObjectOrIRI.IsIRI() {
|
||||||
|
iri := actorObjectOrIRI.GetIRI()
|
||||||
|
if iri == nil {
|
||||||
|
return apActor, errors.New("actor IRI is nil despite IsIRI() returning true")
|
||||||
|
}
|
||||||
|
return GetResolvedActorFromIRI(iri.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return apActor, err
|
if actorObjectOrIRI.IsActivityStreamsPerson() {
|
||||||
|
actorEntity = actorObjectOrIRI.GetActivityStreamsPerson()
|
||||||
|
} else if actorObjectOrIRI.IsActivityStreamsService() {
|
||||||
|
actorEntity = actorObjectOrIRI.GetActivityStreamsService()
|
||||||
|
} else if actorObjectOrIRI.IsActivityStreamsApplication() {
|
||||||
|
actorEntity = actorObjectOrIRI.GetActivityStreamsApplication()
|
||||||
|
} else {
|
||||||
|
return apActor, errors.New("unrecognized external ActivityPub type: " + actorObjectOrIRI.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the external AP entity into an internal actor representation.
|
||||||
|
// NewActivityPubActorFromEntity validates that required fields (ActorIri, Inbox) are present.
|
||||||
|
apa, err := apmodels.NewActivityPubActorFromEntity(actorEntity)
|
||||||
|
if err != nil {
|
||||||
|
return apActor, errors.Wrap(err, "failed to create actor from entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *apa, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResolvedPublicKeyFromIRI will resolve a publicKey IRI string to a vocab.W3IDSecurityV1PublicKey.
|
// GetResolvedPublicKeyFromIRI will resolve a publicKey IRI string to a vocab.W3IDSecurityV1PublicKey.
|
||||||
@@ -190,48 +183,49 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetResolvedActorFromIRI will resolve an IRI string to a fully populated actor.
|
// GetResolvedActorFromIRI will resolve an IRI string to a fully populated actor.
|
||||||
|
// Returns an error if the actor cannot be resolved or is missing required fields.
|
||||||
func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) {
|
func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) {
|
||||||
var err error
|
var resolveErr error
|
||||||
var apActor apmodels.ActivityPubActor
|
var apActor apmodels.ActivityPubActor
|
||||||
resolved := false
|
resolved := false
|
||||||
|
|
||||||
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error {
|
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error {
|
||||||
apa, e := apmodels.MakeActorFromExernalAPEntity(person)
|
apa, e := apmodels.NewActivityPubActorFromEntity(person)
|
||||||
if apa != nil {
|
if e != nil {
|
||||||
apActor = *apa
|
return e
|
||||||
resolved = true
|
|
||||||
}
|
}
|
||||||
return e
|
apActor = *apa
|
||||||
|
resolved = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceCallback := func(c context.Context, service vocab.ActivityStreamsService) error {
|
serviceCallback := func(c context.Context, service vocab.ActivityStreamsService) error {
|
||||||
apa, e := apmodels.MakeActorFromExernalAPEntity(service)
|
apa, e := apmodels.NewActivityPubActorFromEntity(service)
|
||||||
if apa != nil {
|
if e != nil {
|
||||||
apActor = *apa
|
return e
|
||||||
resolved = true
|
|
||||||
}
|
}
|
||||||
return e
|
apActor = *apa
|
||||||
|
resolved = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationCallback := func(c context.Context, app vocab.ActivityStreamsApplication) error {
|
applicationCallback := func(c context.Context, app vocab.ActivityStreamsApplication) error {
|
||||||
apa, e := apmodels.MakeActorFromExernalAPEntity(app)
|
apa, e := apmodels.NewActivityPubActorFromEntity(app)
|
||||||
if apa != nil {
|
if e != nil {
|
||||||
apActor = *apa
|
return e
|
||||||
resolved = true
|
|
||||||
}
|
}
|
||||||
return e
|
apActor = *apa
|
||||||
|
resolved = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if e := ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback, applicationCallback); e != nil {
|
if e := ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback, applicationCallback); e != nil {
|
||||||
err = e
|
resolveErr = errors.Wrap(e, "error resolving actor from IRI")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if !resolved && resolveErr == nil {
|
||||||
err = errors.Wrap(err, "error resolving actor from property value")
|
resolveErr = errors.New("failed to resolve actor from IRI: " + personOrServiceIRI)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resolved {
|
return apActor, resolveErr
|
||||||
err = errors.New("error resolving actor from property value")
|
|
||||||
}
|
|
||||||
|
|
||||||
return apActor, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user