mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-24 08:57:03 -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"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/forgefed"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
//f3 "lab.forgefriends.org/friendlyforgeformat/gof3"
|
||||
"github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
// 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?
|
||||
link := fmt.Sprintf("%s/api/v1/activitypub/repoistory-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID)
|
||||
repository := ap.ApplicationNew(ap.IRI(link))
|
||||
repository.Name.Set("en", ap.Content(ctx.Repo.Repository.Name))
|
||||
repo := forgefed.RepositoryNew(ap.IRI(link))
|
||||
|
||||
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 {
|
||||
ctx.ServerError("MarshalJSON", err)
|
||||
ctx.ServerError("Set Name", 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)
|
||||
}
|
||||
|
||||
response(ctx, repo)
|
||||
}
|
||||
|
||||
// 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