mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-21 08:31:27 -05:00
introduce repo from exosy
This commit is contained in:
parent
157effdd8f
commit
ff5a4405fd
5 changed files with 415 additions and 11 deletions
49
modules/forgefed/forgefed.go
Normal file
49
modules/forgefed/forgefed.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgefed
|
||||||
|
|
||||||
|
import (
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/valyala/fastjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ForgeFedNamespaceURI = "https://forgefed.org/ns"
|
||||||
|
|
||||||
|
// GetItemByType instantiates a new ForgeFed object if the type matches
|
||||||
|
// otherwise it defaults to existing activitypub package typer function.
|
||||||
|
func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) {
|
||||||
|
switch typ {
|
||||||
|
case RepositoryType:
|
||||||
|
return RepositoryNew(""), nil
|
||||||
|
}
|
||||||
|
return ap.GetItemByType(typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONUnmarshalerFn is the function that will load the data from a fastjson.Value into an Item
|
||||||
|
// that the go-ap/activitypub package doesn't know about.
|
||||||
|
func JSONUnmarshalerFn(typ ap.ActivityVocabularyType, val *fastjson.Value, i ap.Item) error {
|
||||||
|
switch typ {
|
||||||
|
case RepositoryType:
|
||||||
|
return OnRepository(i, func(r *Repository) error {
|
||||||
|
return JSONLoadRepository(val, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotEmpty is the function that checks if an object is empty
|
||||||
|
func NotEmpty(i ap.Item) bool {
|
||||||
|
if ap.IsNil(i) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch i.GetType() {
|
||||||
|
case RepositoryType:
|
||||||
|
r, err := ToRepository(i)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ap.NotEmpty(r.Actor)
|
||||||
|
}
|
||||||
|
return ap.NotEmpty(i)
|
||||||
|
}
|
111
modules/forgefed/repository.go
Normal file
111
modules/forgefed/repository.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgefed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/valyala/fastjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepositoryType ap.ActivityVocabularyType = "Repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
ap.Actor
|
||||||
|
// Team Collection of actors who have management/push access to the repository
|
||||||
|
Team ap.Item `jsonld:"team,omitempty"`
|
||||||
|
// Forks OrderedCollection of repositories that are forks of this repository
|
||||||
|
Forks ap.Item `jsonld:"forks,omitempty"`
|
||||||
|
// ForkedFrom Identifies the repository which this repository was created as a fork
|
||||||
|
ForkedFrom ap.Item `jsonld:"forkedFrom,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepositoryNew initializes a Repository type actor
|
||||||
|
func RepositoryNew(id ap.ID) *Repository {
|
||||||
|
a := ap.ActorNew(id, RepositoryType)
|
||||||
|
a.Type = RepositoryType
|
||||||
|
o := Repository{Actor: *a}
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Repository) MarshalJSON() ([]byte, error) {
|
||||||
|
b, err := r.Actor.MarshalJSON()
|
||||||
|
if len(b) == 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = b[:len(b)-1]
|
||||||
|
if r.Team != nil {
|
||||||
|
ap.JSONWriteItemProp(&b, "team", r.Team)
|
||||||
|
}
|
||||||
|
if r.Forks != nil {
|
||||||
|
ap.JSONWriteItemProp(&b, "forks", r.Forks)
|
||||||
|
}
|
||||||
|
if r.ForkedFrom != nil {
|
||||||
|
ap.JSONWriteItemProp(&b, "forkedFrom", r.ForkedFrom)
|
||||||
|
}
|
||||||
|
ap.JSONWrite(&b, '}')
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSONLoadRepository(val *fastjson.Value, r *Repository) error {
|
||||||
|
if err := ap.OnActor(&r.Actor, func(a *ap.Actor) error {
|
||||||
|
return ap.JSONLoadActor(val, a)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Team = ap.JSONGetItem(val, "team")
|
||||||
|
r.Forks = ap.JSONGetItem(val, "forks")
|
||||||
|
r.ForkedFrom = ap.JSONGetItem(val, "forkedFrom")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UnmarshalJSON(data []byte) error {
|
||||||
|
p := fastjson.Parser{}
|
||||||
|
val, err := p.ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return JSONLoadRepository(val, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRepository tries to convert the it Item to a Repository Actor.
|
||||||
|
func ToRepository(it ap.Item) (*Repository, error) {
|
||||||
|
switch i := it.(type) {
|
||||||
|
case *Repository:
|
||||||
|
return i, nil
|
||||||
|
case Repository:
|
||||||
|
return &i, nil
|
||||||
|
case *ap.Actor:
|
||||||
|
return (*Repository)(unsafe.Pointer(i)), nil
|
||||||
|
case ap.Actor:
|
||||||
|
return (*Repository)(unsafe.Pointer(&i)), nil
|
||||||
|
default:
|
||||||
|
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||||
|
typ := reflect.TypeOf(new(Repository))
|
||||||
|
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Repository); ok {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ap.ErrorInvalidType[ap.Actor](it)
|
||||||
|
}
|
||||||
|
|
||||||
|
type withRepositoryFn func(*Repository) error
|
||||||
|
|
||||||
|
// OnRepository calls function fn on it Item if it can be asserted to type *Repository
|
||||||
|
func OnRepository(it ap.Item, fn withRepositoryFn) error {
|
||||||
|
if it == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ob, err := ToRepository(it)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fn(ob)
|
||||||
|
}
|
183
modules/forgefed/repository_test.go
Normal file
183
modules/forgefed/repository_test.go
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgefed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GetItemByType(t *testing.T) {
|
||||||
|
type testtt struct {
|
||||||
|
typ ap.ActivityVocabularyType
|
||||||
|
want ap.Item
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
tests := map[string]testtt{
|
||||||
|
"invalid type": {
|
||||||
|
typ: ap.ActivityVocabularyType("invalidtype"),
|
||||||
|
wantErr: fmt.Errorf("empty ActivityStreams type"), // TODO(marius): this error message needs to be improved in go-ap/activitypub
|
||||||
|
},
|
||||||
|
"Repository": {
|
||||||
|
typ: RepositoryType,
|
||||||
|
want: new(Repository),
|
||||||
|
},
|
||||||
|
"Person - fall back": {
|
||||||
|
typ: ap.PersonType,
|
||||||
|
want: new(ap.Person),
|
||||||
|
},
|
||||||
|
"Question - fall back": {
|
||||||
|
typ: ap.QuestionType,
|
||||||
|
want: new(ap.Question),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tt := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
maybeRepository, err := GetItemByType(tt.typ)
|
||||||
|
if !reflect.DeepEqual(tt.wantErr, err) {
|
||||||
|
t.Errorf("GetItemByType() error = \"%+v\", wantErr = \"%+v\" when getting Item for type %q", tt.wantErr, err, tt.typ)
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(tt.want) != reflect.TypeOf(maybeRepository) {
|
||||||
|
t.Errorf("Invalid type received %T, expected %T", maybeRepository, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RepositoryMarshalJSON(t *testing.T) {
|
||||||
|
type testPair struct {
|
||||||
|
item Repository
|
||||||
|
want []byte
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testPair{
|
||||||
|
"empty": {
|
||||||
|
item: Repository{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
"with ID": {
|
||||||
|
item: Repository{
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
},
|
||||||
|
Team: nil,
|
||||||
|
},
|
||||||
|
want: []byte(`{"id":"https://example.com/1"}`),
|
||||||
|
},
|
||||||
|
"with Team as IRI": {
|
||||||
|
item: Repository{
|
||||||
|
Team: ap.IRI("https://example.com/1"),
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []byte(`{"id":"https://example.com/1","team":"https://example.com/1"}`),
|
||||||
|
},
|
||||||
|
"with Team as IRIs": {
|
||||||
|
item: Repository{
|
||||||
|
Team: ap.ItemCollection{
|
||||||
|
ap.IRI("https://example.com/1"),
|
||||||
|
ap.IRI("https://example.com/2"),
|
||||||
|
},
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []byte(`{"id":"https://example.com/1","team":["https://example.com/1","https://example.com/2"]}`),
|
||||||
|
},
|
||||||
|
"with Team as Object": {
|
||||||
|
item: Repository{
|
||||||
|
Team: ap.Object{ID: "https://example.com/1"},
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []byte(`{"id":"https://example.com/1","team":{"id":"https://example.com/1"}}`),
|
||||||
|
},
|
||||||
|
"with Team as slice of Objects": {
|
||||||
|
item: Repository{
|
||||||
|
Team: ap.ItemCollection{
|
||||||
|
ap.Object{ID: "https://example.com/1"},
|
||||||
|
ap.Object{ID: "https://example.com/2"},
|
||||||
|
},
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []byte(`{"id":"https://example.com/1","team":[{"id":"https://example.com/1"},{"id":"https://example.com/2"}]}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tt := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got, err := tt.item.MarshalJSON()
|
||||||
|
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
|
||||||
|
t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RepositoryUnmarshalJSON(t *testing.T) {
|
||||||
|
type testPair struct {
|
||||||
|
data []byte
|
||||||
|
want *Repository
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testPair{
|
||||||
|
"nil": {
|
||||||
|
data: nil,
|
||||||
|
wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")),
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
data: []byte{},
|
||||||
|
wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")),
|
||||||
|
},
|
||||||
|
"with Type": {
|
||||||
|
data: []byte(`{"type":"Repository"}`),
|
||||||
|
want: &Repository{
|
||||||
|
Actor: ap.Actor{
|
||||||
|
Type: RepositoryType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"with Type and ID": {
|
||||||
|
data: []byte(`{"id":"https://example.com/1","type":"Repository"}`),
|
||||||
|
want: &Repository{
|
||||||
|
Actor: ap.Actor{
|
||||||
|
ID: "https://example.com/1",
|
||||||
|
Type: RepositoryType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tt := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := new(Repository)
|
||||||
|
err := got.UnmarshalJSON(tt.data)
|
||||||
|
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
|
||||||
|
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.want != nil && !reflect.DeepEqual(got, tt.want) {
|
||||||
|
jGot, _ := json.Marshal(got)
|
||||||
|
jWant, _ := json.Marshal(tt.want)
|
||||||
|
t.Errorf("UnmarshalJSON() got = %s, want %s", jGot, jWant)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,14 +8,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/activitypub"
|
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/forgefed"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
//f3 "lab.forgefriends.org/friendlyforgeformat/gof3"
|
//f3 "lab.forgefriends.org/friendlyforgeformat/gof3"
|
||||||
"github.com/go-ap/jsonld"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository function returns the Repository actor for a repo
|
// Repository function returns the Repository actor for a repo
|
||||||
|
@ -37,19 +36,16 @@ func Repository(ctx *context.APIContext) {
|
||||||
|
|
||||||
// TODO: Mabe we should use F3 Repo instead?
|
// TODO: Mabe we should use F3 Repo instead?
|
||||||
link := fmt.Sprintf("%s/api/v1/activitypub/repoistory-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID)
|
link := fmt.Sprintf("%s/api/v1/activitypub/repoistory-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID)
|
||||||
repository := ap.ApplicationNew(ap.IRI(link))
|
repo := forgefed.RepositoryNew(ap.IRI(link))
|
||||||
repository.Name.Set("en", ap.Content(ctx.Repo.Repository.Name))
|
|
||||||
|
|
||||||
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(repository)
|
repo.Name = ap.NaturalLanguageValuesNew()
|
||||||
|
err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("MarshalJSON", err)
|
ctx.ServerError("Set Name", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
|
|
||||||
ctx.Resp.WriteHeader(http.StatusOK)
|
response(ctx, repo)
|
||||||
if _, err = ctx.Resp.Write(binary); err != nil {
|
|
||||||
log.Error("write to resp err: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PersonInbox function handles the incoming data for a repository inbox
|
// PersonInbox function handles the incoming data for a repository inbox
|
||||||
|
|
65
routers/api/v1/activitypub/response.go
Normal file
65
routers/api/v1/activitypub/response.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package activitypub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/activitypub"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/forgefed"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Respond with a ActivityStreams Collection
|
||||||
|
func responseCollection(ctx *context.APIContext, iri string, listOptions db.ListOptions, items []string, count int64) {
|
||||||
|
collection := ap.OrderedCollectionNew(ap.IRI(iri))
|
||||||
|
collection.First = ap.IRI(iri + "?page=1")
|
||||||
|
collection.TotalItems = uint(count)
|
||||||
|
if listOptions.Page == 0 {
|
||||||
|
response(ctx, collection)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := ap.OrderedCollectionPageNew(collection)
|
||||||
|
page.ID = ap.IRI(fmt.Sprintf("%s?page=%d", iri, listOptions.Page))
|
||||||
|
if listOptions.Page > 1 {
|
||||||
|
page.Prev = ap.IRI(fmt.Sprintf("%s?page=%d", iri, listOptions.Page-1))
|
||||||
|
}
|
||||||
|
if listOptions.Page*listOptions.PageSize < int(count) {
|
||||||
|
page.Next = ap.IRI(fmt.Sprintf("%s?page=%d", iri, listOptions.Page+1))
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
err := page.OrderedItems.Append(ap.IRI(item))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Append", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response(ctx, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with an ActivityStreams object
|
||||||
|
func response(ctx *context.APIContext, v interface{}) {
|
||||||
|
binary, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(ap.ActivityBaseURI),
|
||||||
|
jsonld.IRI(ap.SecurityContextURI),
|
||||||
|
jsonld.IRI(forgefed.ForgeFedNamespaceURI),
|
||||||
|
).Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Marshal", 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue