introduce repo from exosy

This commit is contained in:
Michael Jerger 2023-11-06 18:29:48 +01:00
parent 157effdd8f
commit ff5a4405fd
5 changed files with 415 additions and 11 deletions

View 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)
}

View 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)
}

View 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)
}
})
}
}

View file

@ -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

View 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)
}
}