mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-19 08:51:10 +00:00
Sent user activities to distant federated server (#8792)
This PR is part of #4767. It contains * a refactoring of validation error messages * adds the ability to send user-activities to distant federated servers Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8792 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de> Co-committed-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
This commit is contained in:
parent
6b6fa21b25
commit
c081f20776
22 changed files with 885 additions and 130 deletions
|
@ -13,13 +13,6 @@ forgejo.org/models
|
||||||
IsErrSHANotFound
|
IsErrSHANotFound
|
||||||
IsErrMergeDivergingFastForwardOnly
|
IsErrMergeDivergingFastForwardOnly
|
||||||
|
|
||||||
forgejo.org/models/activities
|
|
||||||
GetActivityByID
|
|
||||||
NewFederatedUserActivity
|
|
||||||
CreateUserActivity
|
|
||||||
GetFollowingFeeds
|
|
||||||
FederatedUserActivity.loadActor
|
|
||||||
|
|
||||||
forgejo.org/models/auth
|
forgejo.org/models/auth
|
||||||
WebAuthnCredentials
|
WebAuthnCredentials
|
||||||
|
|
||||||
|
@ -63,8 +56,6 @@ forgejo.org/models/user
|
||||||
GetUserAllSettings
|
GetUserAllSettings
|
||||||
DeleteUserSetting
|
DeleteUserSetting
|
||||||
GetFederatedUser
|
GetFederatedUser
|
||||||
GetFederatedUserByUserID
|
|
||||||
GetFollowersForUser
|
|
||||||
|
|
||||||
forgejo.org/modules/activitypub
|
forgejo.org/modules/activitypub
|
||||||
NewContext
|
NewContext
|
||||||
|
@ -99,16 +90,10 @@ forgejo.org/modules/forgefed
|
||||||
NewForgeUndoLike
|
NewForgeUndoLike
|
||||||
ForgeUndoLike.UnmarshalJSON
|
ForgeUndoLike.UnmarshalJSON
|
||||||
ForgeUndoLike.Validate
|
ForgeUndoLike.Validate
|
||||||
NewForgeUserActivityFromAp
|
|
||||||
NewForgeUserActivity
|
|
||||||
ForgeUserActivity.Validate
|
|
||||||
NewPersonIDFromModel
|
NewPersonIDFromModel
|
||||||
GetItemByType
|
GetItemByType
|
||||||
JSONUnmarshalerFn
|
JSONUnmarshalerFn
|
||||||
NotEmpty
|
NotEmpty
|
||||||
NewForgeUserActivityNoteFromAp
|
|
||||||
newNote
|
|
||||||
ForgeUserActivityNote.Validate
|
|
||||||
ToRepository
|
ToRepository
|
||||||
OnRepository
|
OnRepository
|
||||||
|
|
||||||
|
@ -236,8 +221,8 @@ forgejo.org/routers/web/org
|
||||||
forgejo.org/services/context
|
forgejo.org/services/context
|
||||||
GetPrivateContext
|
GetPrivateContext
|
||||||
|
|
||||||
forgejo.org/services/convert
|
forgejo.org/services/federation
|
||||||
ToActivityPubPersonFeedItem
|
FollowRemoteActor
|
||||||
|
|
||||||
forgejo.org/services/repository
|
forgejo.org/services/repository
|
||||||
IsErrForkAlreadyExist
|
IsErrForkAlreadyExist
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -37,6 +37,8 @@ _testmain.go
|
||||||
|
|
||||||
*coverage.out
|
*coverage.out
|
||||||
coverage.all
|
coverage.all
|
||||||
|
coverage.html
|
||||||
|
coverage.html.gz
|
||||||
coverage/
|
coverage/
|
||||||
cpu.out
|
cpu.out
|
||||||
|
|
||||||
|
@ -129,3 +131,4 @@ prime/
|
||||||
|
|
||||||
# Manpage
|
# Manpage
|
||||||
/man
|
/man
|
||||||
|
tests/integration/api_activitypub_person_inbox_useractivity_test.go
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NewForgeFollowValidation(t *testing.T) {
|
func Test_NewForgeFollowValidation(t *testing.T) {
|
||||||
|
@ -17,15 +18,13 @@ func Test_NewForgeFollowValidation(t *testing.T) {
|
||||||
sut.Actor = ap.IRI("example.org/alice")
|
sut.Actor = ap.IRI("example.org/alice")
|
||||||
sut.Object = ap.IRI("example.org/bob")
|
sut.Object = ap.IRI("example.org/bob")
|
||||||
|
|
||||||
if err, _ := validation.IsValid(sut); !err {
|
valid, err := validation.IsValid(sut)
|
||||||
t.Errorf("sut is invalid: %v\n", err)
|
assert.True(t, valid, "sut is invalid: %v\n", err)
|
||||||
}
|
|
||||||
|
|
||||||
sut = ForgeFollow{}
|
sut = ForgeFollow{}
|
||||||
sut.Actor = ap.IRI("example.org/alice")
|
sut.Actor = ap.IRI("example.org/alice")
|
||||||
sut.Object = ap.IRI("example.org/bob")
|
sut.Object = ap.IRI("example.org/bob")
|
||||||
|
|
||||||
if err, _ := validation.IsValid(sut); err {
|
valid, err = validation.IsValid(sut)
|
||||||
t.Errorf("sut is valid: %v\n", err)
|
assert.False(t, valid, "sut is valid: %v\n", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NewForgeLike(t *testing.T) {
|
func Test_NewForgeLike(t *testing.T) {
|
||||||
|
@ -22,21 +24,14 @@ func Test_NewForgeLike(t *testing.T) {
|
||||||
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
|
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
|
||||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-07")
|
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-07")
|
||||||
sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
|
sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
|
||||||
if err != nil {
|
require.NoError(t, err, "unexpected error: %v\n", err)
|
||||||
t.Errorf("unexpected error: %v\n", err)
|
|
||||||
}
|
valid, _ := validation.IsValid(sut)
|
||||||
if valid, _ := validation.IsValid(sut); !valid {
|
assert.True(t, valid, "sut expected to be valid: %v\n", sut.Validate())
|
||||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := sut.MarshalJSON()
|
got, err := sut.MarshalJSON()
|
||||||
if err != nil {
|
require.NoError(t, err, "MarshalJSON() error = %q", err)
|
||||||
t.Errorf("MarshalJSON() error = \"%v\"", err)
|
assert.True(t, reflect.DeepEqual(got, want), "MarshalJSON()\n got: %q,\n want: %q", got, want)
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("MarshalJSON() got = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_LikeMarshalJSON(t *testing.T) {
|
func Test_LikeMarshalJSON(t *testing.T) {
|
||||||
|
@ -66,13 +61,8 @@ func Test_LikeMarshalJSON(t *testing.T) {
|
||||||
for name, tt := range tests {
|
for name, tt := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
got, err := tt.item.MarshalJSON()
|
got, err := tt.item.MarshalJSON()
|
||||||
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
|
assert.False(t, (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error(), "MarshalJSON()\n got: %v,\n want: %v", err, tt.wantErr)
|
||||||
t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
|
assert.True(t, reflect.DeepEqual(got, tt.want), "MarshalJSON()\n got: %q\n want: %q", got, tt.want)
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,8 +79,8 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
||||||
item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
|
item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
|
||||||
want: &ForgeLike{
|
want: &ForgeLike{
|
||||||
Activity: ap.Activity{
|
Activity: ap.Activity{
|
||||||
Actor: ap.IRI("https://repo.prod.meissa.de/api/activitypub/user-id/1"),
|
|
||||||
Type: "Like",
|
Type: "Like",
|
||||||
|
Actor: ap.IRI("https://repo.prod.meissa.de/api/activitypub/user-id/1"),
|
||||||
Object: ap.IRI("https://codeberg.org/api/activitypub/repository-id/1"),
|
Object: ap.IRI("https://codeberg.org/api/activitypub/repository-id/1"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -107,12 +97,10 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
got := new(ForgeLike)
|
got := new(ForgeLike)
|
||||||
err := got.UnmarshalJSON(test.item)
|
err := got.UnmarshalJSON(test.item)
|
||||||
if (err != nil || test.wantErr != nil) && !strings.Contains(err.Error(), test.wantErr.Error()) {
|
assert.False(t, (err != nil || test.wantErr != nil) && !strings.Contains(err.Error(), test.wantErr.Error()), "UnmarshalJSON()\n error: %v\n wantErr: %v", err, test.wantErr)
|
||||||
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, test.want) {
|
if !reflect.DeepEqual(got, test.want) {
|
||||||
t.Errorf("UnmarshalJSON() got = %q, want %q, err %q", got, test.want, err.Error())
|
assert.Errorf(t, err, "UnmarshalJSON() got = %q, want %q, err %q", got, test.want, err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -120,46 +108,47 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
||||||
|
|
||||||
func Test_ForgeLikeValidation(t *testing.T) {
|
func Test_ForgeLikeValidation(t *testing.T) {
|
||||||
// Successful
|
// Successful
|
||||||
|
|
||||||
sut := new(ForgeLike)
|
sut := new(ForgeLike)
|
||||||
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
||||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||||
if res, _ := validation.IsValid(sut); !res {
|
valid, _ := validation.IsValid(sut)
|
||||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
assert.True(t, valid, "sut expected to be valid: %v\n", sut.Validate())
|
||||||
}
|
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
|
|
||||||
sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||||
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
|
validate := sut.Validate()
|
||||||
t.Error(err)
|
assert.Len(t, validate, 2)
|
||||||
}
|
assert.Equal(t,
|
||||||
|
"Field type contains the value , which is not in allowed subset [Like]",
|
||||||
|
validate[1])
|
||||||
|
|
||||||
sut.UnmarshalJSON([]byte(`{"type":"bad-type",
|
sut.UnmarshalJSON([]byte(`{"type":"bad-type",
|
||||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||||
if err := validateAndCheckError(sut, "Field type contains the value bad-type, which is not in allowed subset [Like]"); err != nil {
|
validate = sut.Validate()
|
||||||
t.Error(err)
|
assert.Len(t, validate, 1)
|
||||||
}
|
assert.Equal(t,
|
||||||
|
"Field type contains the value bad-type, which is not in allowed subset [Like]",
|
||||||
|
validate[0])
|
||||||
|
|
||||||
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
||||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||||
"startTime": "not a date"}`))
|
"startTime": "not a date"}`))
|
||||||
if err := validateAndCheckError(sut, "StartTime was invalid."); err != nil {
|
validate = sut.Validate()
|
||||||
t.Error(err)
|
assert.Len(t, validate, 1)
|
||||||
}
|
assert.Equal(t,
|
||||||
|
"StartTime was invalid.",
|
||||||
|
validate[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActivityValidation_Attack(t *testing.T) {
|
func TestActivityValidation_Attack(t *testing.T) {
|
||||||
sut := new(ForgeLike)
|
sut := new(ForgeLike)
|
||||||
sut.UnmarshalJSON([]byte(`{rubbish}`))
|
sut.UnmarshalJSON([]byte(`{rubbish}`))
|
||||||
if len(sut.Validate()) != 5 {
|
assert.Len(t, sut.Validate(), 5)
|
||||||
t.Errorf("5 validation errors expected but was: %v\n", len(sut.Validate()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,7 +173,7 @@ func TestActivityValidationUndo(t *testing.T) {
|
||||||
"startTime":"2024-03-27T00:00:00Z",
|
"startTime":"2024-03-27T00:00:00Z",
|
||||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||||
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
|
if err := validateAndCheckError(sut, "Value type should not be empty"); err != nil {
|
||||||
t.Error(*err)
|
t.Error(*err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_ForgeUserActivityValidation(t *testing.T) {
|
func Test_ForgeUserActivityValidation(t *testing.T) {
|
||||||
|
@ -34,7 +35,6 @@ func Test_ForgeUserActivityValidation(t *testing.T) {
|
||||||
|
|
||||||
sut.Note = note
|
sut.Note = note
|
||||||
|
|
||||||
if res, _ := validation.IsValid(sut); !res {
|
valid, _ := validation.IsValid(sut)
|
||||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
assert.True(t, valid, "sut expected to be valid: %v\n", sut.Validate())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ func TestPersonIdValidation(t *testing.T) {
|
||||||
|
|
||||||
result, err := validation.IsValid(sut)
|
result, err := validation.IsValid(sut)
|
||||||
assert.False(t, result)
|
assert.False(t, result)
|
||||||
require.EqualError(t, err, "Validation Error: forgefed.PersonID: path should not be empty\npath: \"\" has to be a person specific api path")
|
require.EqualError(t, err, "Validation Error: forgefed.PersonID: Value path should not be empty\npath: \"\" has to be a person specific api path")
|
||||||
|
|
||||||
sut = PersonID{}
|
sut = PersonID{}
|
||||||
sut.ID = "1"
|
sut.ID = "1"
|
||||||
|
@ -166,38 +166,28 @@ func TestWebfingerId(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
|
func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
|
||||||
var err any
|
tests := []struct {
|
||||||
_, err = NewPersonID("", "forgejo")
|
input string
|
||||||
if err == nil {
|
username string
|
||||||
t.Errorf("empty input should be invalid.")
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"", "forgejo", true},
|
||||||
|
{"http://localhost:3000/api/v1/something", "forgejo", true},
|
||||||
|
{"./api/v1/something", "forgejo", true},
|
||||||
|
{"http://1.2.3.4/api/v1/something", "forgejo", true},
|
||||||
|
{"http:///[fe80::1ff:fe23:4567:890a%25eth0]/api/v1/something", "forgejo", true},
|
||||||
|
{"https://codeberg.org/api/v1/activitypub/../activitypub/user-id/12345", "forgejo", true},
|
||||||
|
{"https://myuser@an.other.host/api/v1/activitypub/user-id/1", "forgejo", true},
|
||||||
|
{"https://an.other.host/api/v1/activitypub/user-id/1", "forgejo", false},
|
||||||
}
|
}
|
||||||
_, err = NewPersonID("http://localhost:3000/api/v1/something", "forgejo")
|
|
||||||
if err == nil {
|
for _, tt := range tests {
|
||||||
t.Errorf("localhost uris are not external")
|
_, err := NewPersonID(tt.input, tt.username)
|
||||||
}
|
if tt.expectErr {
|
||||||
_, err = NewPersonID("./api/v1/something", "forgejo")
|
assert.Error(t, err, "Expected an error for input: %s", tt.input)
|
||||||
if err == nil {
|
} else {
|
||||||
t.Errorf("relative uris are not allowed")
|
assert.NoError(t, err, "Expected no error for input: %s, but got: %v", tt.input, err)
|
||||||
}
|
}
|
||||||
_, err = NewPersonID("http://1.2.3.4/api/v1/something", "forgejo")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("uri may not be ip-4 based")
|
|
||||||
}
|
|
||||||
_, err = NewPersonID("http:///[fe80::1ff:fe23:4567:890a%25eth0]/api/v1/something", "forgejo")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("uri may not be ip-6 based")
|
|
||||||
}
|
|
||||||
_, err = NewPersonID("https://codeberg.org/api/v1/activitypub/../activitypub/user-id/12345", "forgejo")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("uri may not contain relative path elements")
|
|
||||||
}
|
|
||||||
_, err = NewPersonID("https://myuser@an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("uri may not contain unparsed elements")
|
|
||||||
}
|
|
||||||
_, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("this uri should be valid but was: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,14 +211,11 @@ func Test_PersonUnmarshalJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
sut := new(ForgePerson)
|
sut := new(ForgePerson)
|
||||||
err := sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`))
|
err := sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`))
|
||||||
if err != nil {
|
require.NoError(t, err, "UnmarshalJSON() unexpected error: %q", err)
|
||||||
t.Errorf("UnmarshalJSON() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
x, _ := expected.MarshalJSON()
|
x, _ := expected.MarshalJSON()
|
||||||
y, _ := sut.MarshalJSON()
|
y, _ := sut.MarshalJSON()
|
||||||
if !reflect.DeepEqual(x, y) {
|
assert.True(t, reflect.DeepEqual(x, y), "UnmarshalJSON()\n got: %q,\n want: %q", x, y)
|
||||||
t.Errorf("UnmarshalJSON() expected: %q got: %q", x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedStr := strings.ReplaceAll(strings.ReplaceAll(`{
|
expectedStr := strings.ReplaceAll(strings.ReplaceAll(`{
|
||||||
"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10",
|
"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10",
|
||||||
|
@ -244,9 +231,7 @@ func Test_PersonUnmarshalJSON(t *testing.T) {
|
||||||
"\n", ""),
|
"\n", ""),
|
||||||
"\t", "")
|
"\t", "")
|
||||||
err = sut.UnmarshalJSON([]byte(expectedStr))
|
err = sut.UnmarshalJSON([]byte(expectedStr))
|
||||||
if err != nil {
|
require.NoError(t, err, "UnmarshalJSON() unexpected error: %q", err)
|
||||||
t.Errorf("UnmarshalJSON() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
result, _ := sut.MarshalJSON()
|
result, _ := sut.MarshalJSON()
|
||||||
assert.JSONEq(t, expectedStr, string(result), "Expected string is not equal")
|
assert.JSONEq(t, expectedStr, string(result), "Expected string is not equal")
|
||||||
}
|
}
|
||||||
|
@ -254,9 +239,8 @@ func Test_PersonUnmarshalJSON(t *testing.T) {
|
||||||
func TestForgePersonValidation(t *testing.T) {
|
func TestForgePersonValidation(t *testing.T) {
|
||||||
sut := new(ForgePerson)
|
sut := new(ForgePerson)
|
||||||
sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`))
|
sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`))
|
||||||
if res, _ := validation.IsValid(sut); !res {
|
valid, _ := validation.IsValid(sut)
|
||||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
assert.True(t, valid, "sut expected to be valid: %v\n", sut.Validate())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAsloginName(t *testing.T) {
|
func TestAsloginName(t *testing.T) {
|
||||||
|
|
|
@ -58,7 +58,7 @@ func TestActorIdValidation(t *testing.T) {
|
||||||
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/"
|
sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/"
|
||||||
result := sut.Validate()
|
result := sut.Validate()
|
||||||
assert.Len(t, result, 1)
|
assert.Len(t, result, 1)
|
||||||
assert.Equal(t, "ID should not be empty", result[0])
|
assert.Equal(t, "Value ID should not be empty", result[0])
|
||||||
|
|
||||||
sut = ActorID{}
|
sut = ActorID{}
|
||||||
sut.ID = "1"
|
sut.ID = "1"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_UserActivityNoteValidation(t *testing.T) {
|
func Test_UserActivityNoteValidation(t *testing.T) {
|
||||||
|
@ -22,7 +23,6 @@ func Test_UserActivityNoteValidation(t *testing.T) {
|
||||||
}
|
}
|
||||||
sut.URL = ap.IRI("example.org/user-id/57")
|
sut.URL = ap.IRI("example.org/user-id/57")
|
||||||
|
|
||||||
if res, _ := validation.IsValid(sut); !res {
|
valid, _ := validation.IsValid(sut)
|
||||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
assert.True(t, valid, "sut expected to be valid: %v\n", sut.Validate())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func IsValid(v Validateable) (bool, error) {
|
||||||
|
|
||||||
func ValidateIDExists(value ap.Item, name string) []string {
|
func ValidateIDExists(value ap.Item, name string) []string {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return []string{fmt.Sprintf("%v should not be nil", name)}
|
return []string{fmt.Sprintf("Field %v must not be nil", name)}
|
||||||
}
|
}
|
||||||
return ValidateNotEmpty(value.GetID().String(), name)
|
return ValidateNotEmpty(value.GetID().String(), name)
|
||||||
}
|
}
|
||||||
|
@ -76,12 +76,12 @@ func ValidateNotEmpty(value any, name string) []string {
|
||||||
if isValid {
|
if isValid {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
return []string{fmt.Sprintf("%v should not be empty", name)}
|
return []string{fmt.Sprintf("Value %v should not be empty", name)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateMaxLen(value string, maxLen int, name string) []string {
|
func ValidateMaxLen(value string, maxLen int, name string) []string {
|
||||||
if utf8.RuneCountInString(value) > maxLen {
|
if utf8.RuneCountInString(value) > maxLen {
|
||||||
return []string{fmt.Sprintf("Value %v was longer than %v", name, maxLen)}
|
return []string{fmt.Sprintf("Value %v is longer than expected length %v", name, maxLen)}
|
||||||
}
|
}
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,12 @@ package activitypub
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forgejo.org/models/activities"
|
||||||
"forgejo.org/modules/activitypub"
|
"forgejo.org/modules/activitypub"
|
||||||
|
"forgejo.org/modules/forgefed"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/web"
|
"forgejo.org/modules/web"
|
||||||
|
"forgejo.org/routers/api/v1/utils"
|
||||||
"forgejo.org/services/context"
|
"forgejo.org/services/context"
|
||||||
"forgejo.org/services/convert"
|
"forgejo.org/services/convert"
|
||||||
"forgejo.org/services/federation"
|
"forgejo.org/services/federation"
|
||||||
|
@ -78,3 +81,162 @@ func PersonInbox(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
responseServiceResult(ctx, result)
|
responseServiceResult(ctx, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PersonFeed returns the recorded activities in the user's feed
|
||||||
|
func PersonFeed(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /activitypub/user-id/{user-id}/outbox activitypub activitypubPersonFeed
|
||||||
|
// ---
|
||||||
|
// summary: List the user's recorded activity
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: user-id
|
||||||
|
// in: path
|
||||||
|
// description: user ID of the user
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/PersonFeed"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
listOptions := utils.GetListOptions(ctx)
|
||||||
|
opts := activities.GetFollowingFeedsOptions{
|
||||||
|
ListOptions: listOptions,
|
||||||
|
}
|
||||||
|
items, count, err := activities.GetFollowingFeeds(ctx, ctx.ContextUser.ID, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetFollowingFeeds", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetTotalCountHeader(count)
|
||||||
|
|
||||||
|
feed := ap.OrderedCollectionNew(ap.IRI(ctx.ContextUser.APActorID() + "/outbox"))
|
||||||
|
feed.AttributedTo = ap.IRI(ctx.ContextUser.APActorID())
|
||||||
|
for _, item := range items {
|
||||||
|
if err := feed.OrderedItems.Append(convert.ToActivityPubPersonFeedItem(item)); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "OrderedItems.Append", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(feed)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("MarshalJSON", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
if _, err = ctx.Resp.Write(binary); err != nil {
|
||||||
|
log.Error("write to resp err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getActivity(ctx *context.APIContext, id int64) (*forgefed.ForgeUserActivity, error) {
|
||||||
|
action, err := activities.GetActivityByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetActivityByID", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.UserID != action.ActUserID || action.ActUserID != ctx.ContextUser.ID {
|
||||||
|
ctx.NotFound()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := activities.ActionList{action}
|
||||||
|
if err := actions.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "action.LoadAttributes", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activity, err := convert.ActionToForgeUserActivity(ctx, actions[0])
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "ActionToForgeUserActivity", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersonActivity returns a user's given activity
|
||||||
|
func PersonActivity(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /activitypub/user-id/{user-id}/activities/{activity-id}/activity activitypub activitypubPersonActivity
|
||||||
|
// ---
|
||||||
|
// summary: Get a specific activity of the user
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: user-id
|
||||||
|
// in: path
|
||||||
|
// description: user ID of the user
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// - name: activity-id
|
||||||
|
// in: path
|
||||||
|
// description: activity ID of the sought activity
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/ActivityPub"
|
||||||
|
|
||||||
|
id := ctx.ParamsInt64("activity-id")
|
||||||
|
activity, err := getActivity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(activity)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("MarshalJSON", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
if _, err = ctx.Resp.Write(binary); err != nil {
|
||||||
|
log.Error("write to resp err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersonActivity returns the Object part of a user's given activity
|
||||||
|
func PersonActivityNote(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /activitypub/user-id/{user-id}/activities/{activity-id} activitypub activitypubPersonActivityNote
|
||||||
|
// ---
|
||||||
|
// summary: Get a specific activity object of the user
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: user-id
|
||||||
|
// in: path
|
||||||
|
// description: user ID of the user
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// - name: activity-id
|
||||||
|
// in: path
|
||||||
|
// description: activity ID of the sought activity
|
||||||
|
// type: integer
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/ActivityPub"
|
||||||
|
|
||||||
|
id := ctx.ParamsInt64("activity-id")
|
||||||
|
activity, err := getActivity(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(activity.Object)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("MarshalJSON", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
if _, err = ctx.Resp.Write(binary); err != nil {
|
||||||
|
log.Error("write to resp err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -844,6 +844,11 @@ func Routes() *web.Route {
|
||||||
activitypub.ReqHTTPUserSignature(),
|
activitypub.ReqHTTPUserSignature(),
|
||||||
bind(ap.Activity{}),
|
bind(ap.Activity{}),
|
||||||
activitypub.PersonInbox)
|
activitypub.PersonInbox)
|
||||||
|
m.Group("/activities/{activity-id}", func() {
|
||||||
|
m.Get("", activitypub.PersonActivityNote)
|
||||||
|
m.Get("/activity", activitypub.PersonActivity)
|
||||||
|
})
|
||||||
|
m.Get("/outbox", activitypub.ReqHTTPUserSignature(), activitypub.PersonFeed)
|
||||||
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
||||||
m.Group("/actor", func() {
|
m.Group("/actor", func() {
|
||||||
m.Get("", activitypub.Actor)
|
m.Get("", activitypub.Actor)
|
||||||
|
|
|
@ -13,3 +13,10 @@ type swaggerResponseActivityPub struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body api.ActivityPub `json:"body"`
|
Body api.ActivityPub `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Personfeed
|
||||||
|
// swagger:response PersonFeed
|
||||||
|
type swaggerResponsePersonFeed struct {
|
||||||
|
// in:body
|
||||||
|
Body []api.APPersonFollowItem `json:"body"`
|
||||||
|
}
|
||||||
|
|
177
services/convert/activitypub_user_action.go
Normal file
177
services/convert/activitypub_user_action.go
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
activities_model "forgejo.org/models/activities"
|
||||||
|
issues_model "forgejo.org/models/issues"
|
||||||
|
fm "forgejo.org/modules/forgefed"
|
||||||
|
"forgejo.org/modules/json"
|
||||||
|
"forgejo.org/modules/markup"
|
||||||
|
"forgejo.org/modules/markup/markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ActionToForgeUserActivity(ctx context.Context, action *activities_model.Action) (fm.ForgeUserActivity, error) {
|
||||||
|
render := func(format string, args ...any) string {
|
||||||
|
return fmt.Sprintf(`<a href="%s" rel="nofollow">%s</a> %s`, action.ActUser.HTMLURL(), action.GetActDisplayName(ctx), fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
renderIssue := func(issue *issues_model.Issue) string {
|
||||||
|
return fmt.Sprintf(`<a href="%s" rel="nofollow">%s#%d</a>`,
|
||||||
|
issue.HTMLURL(),
|
||||||
|
action.GetRepoPath(ctx),
|
||||||
|
issue.Index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderRepo := func() string {
|
||||||
|
return fmt.Sprintf(`<a href="%s" rel="nofollow">%s</a>`, action.Repo.HTMLURL(), action.GetRepoPath(ctx))
|
||||||
|
}
|
||||||
|
renderBranch := func() string {
|
||||||
|
return fmt.Sprintf(`<a href="%s" rel="nofollow">%s</a>`, action.GetRefLink(ctx), action.GetBranch())
|
||||||
|
}
|
||||||
|
renderTag := func() string {
|
||||||
|
return fmt.Sprintf(`<a href="%s" rel="nofollow">%s</a>`, action.GetRefLink(ctx), action.GetTag())
|
||||||
|
}
|
||||||
|
|
||||||
|
makeUserActivity := func(format string, args ...any) (fm.ForgeUserActivity, error) {
|
||||||
|
return fm.NewForgeUserActivity(action.ActUser, action.ID, render(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action.OpType {
|
||||||
|
case activities_model.ActionCreateRepo:
|
||||||
|
return makeUserActivity("created a new repository: %s", renderRepo())
|
||||||
|
case activities_model.ActionRenameRepo:
|
||||||
|
return makeUserActivity("renamed a repository: %s", renderRepo())
|
||||||
|
case activities_model.ActionStarRepo:
|
||||||
|
return makeUserActivity("starred a repository: %s", renderRepo())
|
||||||
|
case activities_model.ActionWatchRepo:
|
||||||
|
return makeUserActivity("started watching a repository: %s", renderRepo())
|
||||||
|
case activities_model.ActionCommitRepo:
|
||||||
|
type PushCommit struct {
|
||||||
|
Sha1 string
|
||||||
|
Message string
|
||||||
|
AuthorEmail string
|
||||||
|
AuthorName string
|
||||||
|
CommitterEmail string
|
||||||
|
CommitterName string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
type PushCommits struct {
|
||||||
|
Commits []*PushCommit
|
||||||
|
HeadCommit *PushCommit
|
||||||
|
CompareURL string
|
||||||
|
Len int
|
||||||
|
}
|
||||||
|
|
||||||
|
commits := &PushCommits{}
|
||||||
|
if err := json.Unmarshal([]byte(action.GetContent()), commits); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
commitsHTML := ""
|
||||||
|
renderCommit := func(commit *PushCommit) string {
|
||||||
|
return fmt.Sprintf(`<li><a href="%s" rel="nofollow">%s</a> <pre>%s</pre></li>`,
|
||||||
|
fmt.Sprintf("%s/commit/%s", action.GetRepoAbsoluteLink(ctx), url.PathEscape(commit.Sha1)),
|
||||||
|
commit.Sha1,
|
||||||
|
html.EscapeString(commit.Message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for _, commit := range commits.Commits {
|
||||||
|
commitsHTML += renderCommit(commit)
|
||||||
|
}
|
||||||
|
return makeUserActivity("pushed to %s at %s: <ul>%s</ul>", renderBranch(), renderRepo(), commitsHTML)
|
||||||
|
case activities_model.ActionCreateIssue:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("opened issue %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionCreatePullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("opened pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionTransferRepo:
|
||||||
|
return makeUserActivity("transferred %s", renderRepo())
|
||||||
|
case activities_model.ActionPushTag:
|
||||||
|
return makeUserActivity("pushed %s at %s", renderTag(), renderRepo())
|
||||||
|
case activities_model.ActionCommentIssue:
|
||||||
|
renderedComment, err := markdown.RenderString(&markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
|
}, action.Comment.Content)
|
||||||
|
if err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeUserActivity(`<a href="%s" rel="nofollow">commented</a> on %s: <blockquote>%s</blockquote>`,
|
||||||
|
action.GetCommentHTMLURL(ctx),
|
||||||
|
renderIssue(action.Comment.Issue),
|
||||||
|
renderedComment,
|
||||||
|
)
|
||||||
|
case activities_model.ActionMergePullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("merged pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionCloseIssue:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("closed issue %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionReopenIssue:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("reopened issue %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionClosePullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("closed pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionReopenPullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("reopened pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionDeleteTag:
|
||||||
|
return makeUserActivity("deleted tag %s at %s", action.GetTag(), renderRepo())
|
||||||
|
case activities_model.ActionDeleteBranch:
|
||||||
|
return makeUserActivity("deleted branch %s at %s", action.GetBranch(), renderRepo())
|
||||||
|
case activities_model.ActionApprovePullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("approved pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionRejectPullRequest:
|
||||||
|
if err := action.LoadIssue(ctx); err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
return makeUserActivity("rejected pull request %s", renderIssue(action.Issue))
|
||||||
|
case activities_model.ActionCommentPull:
|
||||||
|
renderedComment, err := markdown.RenderString(&markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
|
}, action.Comment.Content)
|
||||||
|
if err != nil {
|
||||||
|
return fm.ForgeUserActivity{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeUserActivity(`<a href="%s" rel="nofollow">commented</a> on %s: <blockquote>%s</blockquote>`,
|
||||||
|
action.GetCommentHTMLURL(ctx),
|
||||||
|
renderIssue(action.Comment.Issue),
|
||||||
|
renderedComment,
|
||||||
|
)
|
||||||
|
case activities_model.ActionMirrorSyncPush:
|
||||||
|
case activities_model.ActionMirrorSyncCreate:
|
||||||
|
case activities_model.ActionMirrorSyncDelete:
|
||||||
|
case activities_model.ActionPublishRelease:
|
||||||
|
case activities_model.ActionPullReviewDismissed:
|
||||||
|
case activities_model.ActionPullRequestReadyForReview:
|
||||||
|
case activities_model.ActionAutoMergePullRequest:
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeUserActivity("performed an unrecognised action: %s", action.OpType.String())
|
||||||
|
}
|
22
services/federation/person_inbox_accept.go
Normal file
22
services/federation/person_inbox_accept.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package federation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forgejo.org/modules/log"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processPersonInboxAccept(activity *ap.Activity) (ServiceResult, error) {
|
||||||
|
if activity.Object.GetType() != ap.FollowType {
|
||||||
|
log.Error("Invalid object type for Accept activity: %v", activity.Object.GetType())
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("invalid object type for Accept activity: %v", activity.Object.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We currently do not do anything here, we just drop it.
|
||||||
|
return NewServiceResultStatusOnly(http.StatusNoContent), nil
|
||||||
|
}
|
55
services/federation/person_inbox_create.go
Normal file
55
services/federation/person_inbox_create.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package federation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forgejo.org/models/activities"
|
||||||
|
"forgejo.org/models/user"
|
||||||
|
fm "forgejo.org/modules/forgefed"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processPersonInboxCreate(ctx context.Context, user *user.User, activity *ap.Activity) (ServiceResult, error) {
|
||||||
|
createAct, err := fm.NewForgeUserActivityFromAp(*activity)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Invalid user activity: %v, %v", activity, err)
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("Invalid user activity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actorURI := createAct.Actor.GetLink().String()
|
||||||
|
federatedBaseUser, _, _, err := findFederatedUser(ctx, actorURI)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Federated user not found (%s): %v", actorURI, err)
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("federated user not found (%s): %v", actorURI, err)
|
||||||
|
}
|
||||||
|
if federatedBaseUser == nil {
|
||||||
|
log.Error("Federated user not found (%s): %v", actorURI, err)
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("federated user not found (%s): %v", actorURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
federatedUserActivity, err := activities.NewFederatedUserActivity(
|
||||||
|
user.ID,
|
||||||
|
federatedBaseUser.ID,
|
||||||
|
createAct.Actor.GetLink().String(),
|
||||||
|
createAct.Note.Content.String(),
|
||||||
|
createAct.Note.URL.GetID().String(),
|
||||||
|
*activity,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error creating federatedUserActivity (%s): %v", actorURI, err)
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("Error creating federatedUserActivity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := activities.CreateUserActivity(ctx, &federatedUserActivity); err != nil {
|
||||||
|
log.Error("Unable to record activity: %v", err)
|
||||||
|
return ServiceResult{}, NewErrNotAcceptablef("Unable to record activity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewServiceResultStatusOnly(http.StatusNoContent), nil
|
||||||
|
}
|
|
@ -5,21 +5,56 @@ package federation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"forgejo.org/models/user"
|
"forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/forgefed"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
|
context_service "forgejo.org/services/context"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProcessPersonInbox(ctx context.Context, user *user.User, activity *ap.Activity) (ServiceResult, error) {
|
func ProcessPersonInbox(ctx context.Context, user *user.User, activity *ap.Activity) (ServiceResult, error) {
|
||||||
switch activity.Type {
|
switch activity.Type {
|
||||||
|
case ap.CreateType:
|
||||||
|
return processPersonInboxCreate(ctx, user, activity)
|
||||||
case ap.FollowType:
|
case ap.FollowType:
|
||||||
return processPersonFollow(ctx, user, activity)
|
return processPersonFollow(ctx, user, activity)
|
||||||
case ap.UndoType:
|
case ap.UndoType:
|
||||||
return processPersonInboxUndo(ctx, user, activity)
|
return processPersonInboxUndo(ctx, user, activity)
|
||||||
|
case ap.AcceptType:
|
||||||
|
return processPersonInboxAccept(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error("Unsupported PersonInbox activity: %v", activity.Type)
|
log.Error("Unsupported PersonInbox activity: %v", activity.Type)
|
||||||
return ServiceResult{}, NewErrNotAcceptablef("unsupported activity: %v", activity.Type)
|
return ServiceResult{}, NewErrNotAcceptablef("unsupported activity: %v", activity.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FollowRemoteActor(ctx *context_service.APIContext, localUser *user.User, actorURI string) error {
|
||||||
|
_, federatedUser, federationHost, err := FindOrCreateFederatedUser(ctx.Base, actorURI)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Federated user not found (%s): %v", actorURI, err)
|
||||||
|
ctx.Error(http.StatusNotAcceptable, "Federated user not found", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
followReq, err := forgefed.NewForgeFollow(localUser.APActorID(), actorURI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI)).
|
||||||
|
Marshal(followReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostURL := federationHost.AsURL()
|
||||||
|
return deliveryQueue.Push(deliveryQueueItem{
|
||||||
|
InboxURL: hostURL.JoinPath(federatedUser.InboxPath).String(),
|
||||||
|
Doer: localUser,
|
||||||
|
Payload: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
83
services/federation/user_activity.go
Normal file
83
services/federation/user_activity.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package federation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
activities_model "forgejo.org/models/activities"
|
||||||
|
"forgejo.org/models/forgefed"
|
||||||
|
"forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/structs"
|
||||||
|
"forgejo.org/services/convert"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendUserActivity(ctx context.Context, doer *user.User, activity *activities_model.Action) error {
|
||||||
|
followers, err := user.GetFollowersForUser(ctx, doer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userActivity, err := convert.ActionToForgeUserActivity(ctx, activity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(ap.ActivityBaseURI),
|
||||||
|
).Marshal(userActivity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follower := range followers {
|
||||||
|
_, federatedUserFollower, err := user.GetFederatedUserByUserID(ctx, follower.FollowingUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
federationHost, err := forgefed.GetFederationHost(ctx, federatedUserFollower.FederationHostID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostURL := federationHost.AsURL()
|
||||||
|
if err := deliveryQueue.Push(deliveryQueueItem{
|
||||||
|
InboxURL: hostURL.JoinPath(federatedUserFollower.InboxPath).String(),
|
||||||
|
Doer: doer,
|
||||||
|
Payload: payload,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotifyActivityPubFollowers(ctx context.Context, actions []activities_model.Action) error {
|
||||||
|
if !setting.Federation.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, act := range actions {
|
||||||
|
if act.Repo != nil {
|
||||||
|
if act.Repo.IsPrivate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if act.Repo.Owner.KeepActivityPrivate || act.Repo.Owner.Visibility != structs.VisibleTypePublic {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if act.ActUser.KeepActivityPrivate || act.ActUser.Visibility != structs.VisibleTypePublic {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := SendUserActivity(ctx, act.ActUser, &act); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"forgejo.org/modules/repository"
|
"forgejo.org/modules/repository"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
"forgejo.org/modules/util"
|
"forgejo.org/modules/util"
|
||||||
|
federation_service "forgejo.org/services/federation"
|
||||||
notify_service "forgejo.org/services/notify"
|
notify_service "forgejo.org/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,21 +41,19 @@ func NewNotifier() notify_service.Notifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyAll(ctx context.Context, action *activities_model.Action) error {
|
func notifyAll(ctx context.Context, action *activities_model.Action) error {
|
||||||
_, err := activities_model.NotifyWatchers(ctx, action)
|
out, err := activities_model.NotifyWatchers(ctx, action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return err
|
return federation_service.NotifyActivityPubFollowers(ctx, out)
|
||||||
// return federation_service.NotifyActivityPubFollowers(ctx, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error {
|
func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error {
|
||||||
_, err := activities_model.NotifyWatchersActions(ctx, acts)
|
out, err := activities_model.NotifyWatchersActions(ctx, acts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return federation_service.NotifyActivityPubFollowers(ctx, out)
|
||||||
// return federation_service.NotifyActivityPubFollowers(ctx, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
||||||
|
|
126
templates/swagger/v1_json.tmpl
generated
126
templates/swagger/v1_json.tmpl
generated
|
@ -142,6 +142,72 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/activitypub/user-id/{user-id}/activities/{activity-id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
"summary": "Get a specific activity object of the user",
|
||||||
|
"operationId": "activitypubPersonActivityNote",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "user ID of the user",
|
||||||
|
"name": "user-id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "activity ID of the sought activity",
|
||||||
|
"name": "activity-id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ActivityPub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/activitypub/user-id/{user-id}/activities/{activity-id}/activity": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
"summary": "Get a specific activity of the user",
|
||||||
|
"operationId": "activitypubPersonActivity",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "user ID of the user",
|
||||||
|
"name": "user-id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "activity ID of the sought activity",
|
||||||
|
"name": "activity-id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ActivityPub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/activitypub/user-id/{user-id}/inbox": {
|
"/activitypub/user-id/{user-id}/inbox": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -168,6 +234,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/activitypub/user-id/{user-id}/outbox": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
"summary": "List the user's recorded activity",
|
||||||
|
"operationId": "activitypubPersonFeed",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "user ID of the user",
|
||||||
|
"name": "user-id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/PersonFeed"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/responses/forbidden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/cron": {
|
"/admin/cron": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -21121,6 +21216,28 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "forgejo.org/services/context"
|
"x-go-package": "forgejo.org/services/context"
|
||||||
},
|
},
|
||||||
|
"APPersonFollowItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"actor_id": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "ActorID"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Note"
|
||||||
|
},
|
||||||
|
"original_item": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "OriginalItem"
|
||||||
|
},
|
||||||
|
"original_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "OriginalURL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
|
},
|
||||||
"AccessToken": {
|
"AccessToken": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "AccessToken represents an API access token.",
|
"title": "AccessToken represents an API access token.",
|
||||||
|
@ -29566,6 +29683,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"PersonFeed": {
|
||||||
|
"description": "Personfeed",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/APPersonFollowItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PublicKey": {
|
"PublicKey": {
|
||||||
"description": "PublicKey",
|
"description": "PublicKey",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
// Copyright 2022, 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
auth_model "forgejo.org/models/auth"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/activitypub"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/structs"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/routers"
|
||||||
|
"forgejo.org/services/contexttest"
|
||||||
|
"forgejo.org/services/federation"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActivityPubPersonInboxNoteToDistant(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
federation.Init()
|
||||||
|
|
||||||
|
mock := test.NewFederationServerMock()
|
||||||
|
federatedSrv := mock.DistantServer(t)
|
||||||
|
defer federatedSrv.Close()
|
||||||
|
|
||||||
|
onGiteaRun(t, func(t *testing.T, localUrl *url.URL) {
|
||||||
|
defer test.MockVariableValue(&setting.AppURL, localUrl.String())()
|
||||||
|
|
||||||
|
distantURL := federatedSrv.URL
|
||||||
|
distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
||||||
|
|
||||||
|
localUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
localUser2URL := localUrl.JoinPath("/api/v1/activitypub/user-id/2").String()
|
||||||
|
localUser2Inbox := fmt.Sprintf("%v/inbox", localUser2URL)
|
||||||
|
localSession2 := loginUser(t, localUser2.LoginName)
|
||||||
|
localSecssion2Token := getTokenForLoggedInUser(t, localSession2, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
|
||||||
|
repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, localUser2, tests.DeclarativeRepoOptions{})
|
||||||
|
defer f()
|
||||||
|
|
||||||
|
// follow (distant follows local)
|
||||||
|
followActivity := []byte(fmt.Sprintf(
|
||||||
|
`{"type":"Follow",`+
|
||||||
|
`"actor":"%s",`+
|
||||||
|
`"object":"%s"}`,
|
||||||
|
distantUser15URL,
|
||||||
|
localUser2URL,
|
||||||
|
))
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, localUser2Inbox)
|
||||||
|
cf, err := activitypub.NewClientFactoryWithTimeout(60 * time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey,
|
||||||
|
mock.ApActor.KeyID(federatedSrv.URL))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := c.Post(followActivity, localUser2Inbox)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||||
|
|
||||||
|
// local action which triggers a user activity
|
||||||
|
IssueURL := fmt.Sprintf("/api/v1/repos/%s/issues?state=all", repo.FullName())
|
||||||
|
req := NewRequestWithJSON(t, "POST", IssueURL, &structs.CreateIssueOption{
|
||||||
|
Title: "ActivityFeed test",
|
||||||
|
Body: "Nothing to see here!",
|
||||||
|
}).AddTokenAuth(localSecssion2Token)
|
||||||
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
// distant request outbox
|
||||||
|
localUser2Outbox := fmt.Sprintf("%v/outbox", localUser2URL)
|
||||||
|
resp, err = c.Get(localUser2Outbox)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// distant request activity & activity note
|
||||||
|
localUser2ActivityNote := fmt.Sprintf("%v/activities/1", localUser2URL)
|
||||||
|
localUser2Activity := fmt.Sprintf("%v/activities/1/activity", localUser2URL)
|
||||||
|
resp, err = c.Get(localUser2ActivityNote)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
resp, err = c.Get(localUser2Activity)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// check for activity on distant inbox
|
||||||
|
assert.Contains(t, mock.LastPost, "user2</a> opened issue")
|
||||||
|
})
|
||||||
|
}
|
|
@ -39,9 +39,6 @@ func TestActivityPubPerson(t *testing.T) {
|
||||||
localUserName := "user2"
|
localUserName := "user2"
|
||||||
localUserURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", localUrl, localUserID)
|
localUserURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", localUrl, localUserID)
|
||||||
|
|
||||||
// distantURL := federatedSrv.URL
|
|
||||||
// distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
|
||||||
|
|
||||||
// Unsigned request
|
// Unsigned request
|
||||||
t.Run("UnsignedRequest", func(t *testing.T) {
|
t.Run("UnsignedRequest", func(t *testing.T) {
|
||||||
req := NewRequest(t, "GET", localUserURL)
|
req := NewRequest(t, "GET", localUserURL)
|
||||||
|
@ -111,3 +108,30 @@ func TestActivityPubPersonInbox(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusNotAcceptable, resp.StatusCode)
|
assert.Equal(t, http.StatusNotAcceptable, resp.StatusCode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActivityPubPersonOutbox(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
mock := test.NewFederationServerMock()
|
||||||
|
federatedSrv := mock.DistantServer(t)
|
||||||
|
defer federatedSrv.Close()
|
||||||
|
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
defer test.MockVariableValue(&setting.AppURL, u.String())()
|
||||||
|
user2outboxurl := u.JoinPath("/api/v1/activitypub/user-id/2/outbox").String()
|
||||||
|
|
||||||
|
ctx, _ := contexttest.MockAPIContext(t, user2outboxurl)
|
||||||
|
cf, err := activitypub.NewClientFactoryWithTimeout(60 * time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c, err := cf.WithKeysDirect(ctx, mock.Persons[0].PrivKey,
|
||||||
|
mock.Persons[0].KeyID(federatedSrv.URL))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// request outbox
|
||||||
|
resp, err := c.Get(user2outboxurl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue