Changes
7 changed files (+617/-33)
-
models/forgefed/forgefed.go (deleted)
-
@@ -1,26 +0,0 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package forgefed import ( ap "github.com/go-ap/activitypub" ) const ( RepositoryType ap.ActivityVocabularyType = "Repository" ) type Repository struct { ap.Actor // Team specifies a Collection of actors who are working on the object Team ap.Item `jsonld:"team,omitempty"` } // RepositoryNew initializes a Repository type actor func RepositoryNew(id ap.ID) *Repository { a := ap.ActorNew(id, RepositoryType) o := Repository{Actor: *a} return &o }
-
-
-
@@ -0,0 +1,72 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package forgefed import ( "github.com/valyala/fastjson" ap "github.com/go-ap/activitypub" ) 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"` } // GetItemByType instantiates a new Repository object if the type matches // otherwise it defaults to existing activitypub package typer function. func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) { if typ == RepositoryType { return RepositoryNew(""), nil } return ap.GetItemByType(typ) } // RepositoryNew initializes a Repository type actor func RepositoryNew(id ap.ID) *Repository { a := ap.ActorNew(id, RepositoryType) o := Repository{Actor: *a} o.Type = RepositoryType return &o } func (r Repository) MarshalJSON() ([]byte, error) { b, err := r.Actor.MarshalJSON() if len(b) == 0 || err != nil { return make([]byte, 0), err } b = b[:len(b)-1] if r.Team != nil { ap.WriteItemJSONProp(&b, "team", r.Team) } if r.Forks != nil { ap.WriteItemJSONProp(&b, "forks", r.Forks) } ap.Write(&b, '}') return b, nil } func (r *Repository) UnmarshalJSON(data []byte) error { p := fastjson.Parser{} val, err := p.ParseBytes(data) if err != nil { return err } r.Team = ap.JSONGetItem(val, "team") r.Forks = ap.JSONGetItem(val, "forks") return ap.OnActor(&r.Actor, func(a *ap.Actor) error { return ap.LoadActor(val, a) }) }
-
-
-
@@ -0,0 +1,167 @@package forgefed import ( "encoding/json" "fmt" "reflect" "testing" 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"), }, want: []byte(`{"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"), }, }, want: []byte(`{"team":["https://example.com/1","https://example.com/2"]}`), }, "with Team as Object": { item: Repository{ Team: ap.Object{ID: "https://example.com/1"}, }, want: []byte(`{"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"}, }, }, want: []byte(`{"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) } }) } }
-
-
-
@@ -10,6 +10,8 @@ "net/http""strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context"
-
@@ -82,7 +84,7 @@ person.PublicKey.PublicKeyPem = publicKeyPembinary, err := person.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) ctx.Error(http.StatusInternalServerError, "MarshalJSON", err) return } response(ctx, binary)
-
@@ -166,7 +168,7 @@ outbox.TotalItems = uint(len(outbox.OrderedItems))binary, err := outbox.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) ctx.Error(http.StatusInternalServerError, "MarshalJSON", err) } response(ctx, binary) }
-
@@ -207,7 +209,7 @@ }binary, err := following.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) ctx.Error(http.StatusInternalServerError, "MarshalJSON", err) } response(ctx, binary) }
-
@@ -247,7 +249,51 @@ }binary, err := followers.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) ctx.Error(http.StatusInternalServerError, "MarshalJSON", err) } response(ctx, binary) } // PersonLiked function returns the user's Liked Collection func PersonLiked(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/followers activitypub activitypubPersonLiked // --- // summary: Returns the Liked Collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{ Actor: ctx.Doer, Private: ctx.IsSigned, StarredByID: ctx.ContextUser.ID, }) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserStarred", err) return } liked := ap.OrderedCollectionNew(ap.IRI(link)) liked.TotalItems = uint(count) for _, repo := range repos { repo := forgefed.RepositoryNew(ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + repo.OwnerName + "/" + repo.Name)) liked.OrderedItems.Append(repo) } binary, err := liked.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "MarshalJSON", err) } response(ctx, binary) }
-
-
-
@@ -0,0 +1,212 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "io" "net/http" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" //repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/api/v1/utils" ap "github.com/go-ap/activitypub" ) // Repo function func Repo(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/{reponame} activitypub activitypubRepo // --- // summary: Returns the repository // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") repo := forgefed.RepositoryNew(ap.IRI(link)) repo.Name = ap.NaturalLanguageValuesNew() err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name)) if err != nil { ctx.Error(http.StatusInternalServerError, "Set Name", err) return } repo.AttributedTo = ap.IRI(strings.TrimSuffix(link, "/"+ctx.Repo.Repository.Name)) repo.Summary = ap.NaturalLanguageValuesNew() err = repo.Summary.Set("en", ap.Content(ctx.Repo.Repository.Description)) if err != nil { ctx.Error(http.StatusInternalServerError, "Set Description", err) return } repo.Inbox = ap.IRI(link + "/inbox") repo.Outbox = ap.IRI(link + "/outbox") repo.Followers = ap.IRI(link + "/followers") repo.Team = ap.IRI(link + "/team") binary, err := repo.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) return } response(ctx, binary) } // RepoInbox function func RepoInbox(ctx *context.APIContext) { // swagger:operation POST /activitypub/user/{username}/{reponame}/inbox activitypub activitypubRepoInbox // --- // summary: Send to the inbox // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "204": // "$ref": "#/responses/empty" body, err := io.ReadAll(ctx.Req.Body) if err != nil { ctx.Error(http.StatusInternalServerError, "Error reading request body", err) } var activity ap.Activity activity.UnmarshalJSON(body) if activity.Type == ap.FollowType { //activitypub.Follow(ctx, activity) } else { log.Warn("ActivityStreams type not supported", activity) } ctx.Status(http.StatusNoContent) } // RepoOutbox function func RepoOutbox(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/outbox activitypub activitypubPersonOutbox // --- // summary: Returns the outbox // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.ContextUser, IncludePrivate: false, OnlyPerformedBy: true, IncludeDeleted: false, Date: ctx.FormString("date"), }) if err != nil { ctx.Error(http.StatusInternalServerError, "Couldn't fetch outbox", err) } outbox := ap.OrderedCollectionNew(ap.IRI(link)) for _, action := range feed { /*if action.OpType == ExampleType { activity := ap.ExampleNew() outbox.OrderedItems.Append(activity) }*/ log.Debug(action.Content) } outbox.TotalItems = uint(len(outbox.OrderedItems)) binary, err := outbox.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) } response(ctx, binary) } // RepoFollowers function func RepoFollowers(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username}/{reponame}/followers activitypub activitypubRepoFollowers // --- // summary: Returns the followers collection // produces: // - application/activity+json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // - name: reponame // in: path // description: name of the repository // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ctx.Req.URL.EscapedPath(), "/") users, err := user_model.GetUserFollowers(ctx.ContextUser, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) return } followers := ap.OrderedCollectionNew(ap.IRI(link)) followers.TotalItems = uint(len(users)) for _, user := range users { person := ap.PersonNew(ap.IRI(user.Website)) followers.OrderedItems.Append(person) } binary, err := followers.MarshalJSON() if err != nil { ctx.Error(http.StatusInternalServerError, "Serialize", err) } response(ctx, binary) }
-
-
-
@@ -651,6 +651,13 @@ m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)m.Get("/outbox", activitypub.PersonOutbox) m.Get("/following", activitypub.PersonFollowing) m.Get("/followers", activitypub.PersonFollowers) m.Get("/liked", activitypub.PersonLiked) m.Group("/{reponame}", func() { m.Get("", activitypub.Repo) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.RepoInbox) m.Get("/outbox", activitypub.RepoOutbox) m.Get("/followers", activitypub.RepoFollowers) }, repoAssignment()) }, context_service.UserAssignmentAPI()) }) }
-
-
-
@@ -57,8 +57,8 @@ ],"tags": [ "activitypub" ], "summary": "Returns the followers collection", "operationId": "activitypubPersonFollowers", "summary": "Returns the Liked Collection", "operationId": "activitypubPersonLiked", "parameters": [ { "type": "string",
-
@@ -83,7 +83,7 @@ ],"tags": [ "activitypub" ], "summary": "Returns the following collection", "summary": "Returns the Following Collection", "operationId": "activitypubPersonFollowing", "parameters": [ {
-
@@ -144,11 +144,117 @@ "description": "username of the user","name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/{reponame}": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the repository", "operationId": "activitypubRepo", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/{reponame}/followers": { "get": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Returns the followers collection", "operationId": "activitypubRepoFollowers", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/{reponame}/inbox": { "post": { "produces": [ "application/activity+json" ], "tags": [ "activitypub" ], "summary": "Send to the inbox", "operationId": "activitypubRepoInbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true }, { "type": "string", "description": "name of the repository", "name": "reponame", "in": "path", "required": true } ], "responses": { "204": { "$ref": "#/responses/empty" } } }
-