From fd4d0e730e9648a99d602400bcec5b7a9064a5a7 Mon Sep 17 00:00:00 2001 From: Anthony Wang Date: Sun, 27 Nov 2022 00:34:24 +0000 Subject: [PATCH] Move AS object processing to routers/api/v1/activitypub, move AP transport and IRI code to services/activitypub This is to follow https://docs.gitea.io/en-us/guidelines-backend/ and avoid import cycles. --- modules/activitypub/comment.go | 42 ---- modules/activitypub/issue.go | 47 ----- modules/activitypub/repo.go | 44 ---- .../v1/activitypub}/authorize_interaction.go | 35 +-- routers/api/v1/activitypub/create.go | 199 ++++++++++++++++++ .../api/v1}/activitypub/follow.go | 26 ++- routers/api/v1/activitypub/fork.go | 51 +++++ routers/api/v1/activitypub/person.go | 6 +- .../api/v1}/activitypub/pull_request.go | 5 +- routers/api/v1/activitypub/repo.go | 11 +- routers/api/v1/activitypub/reqsignature.go | 5 +- routers/api/v1/activitypub/response.go | 2 +- .../api/v1}/activitypub/star.go | 7 +- routers/api/v1/api.go | 1 + routers/web/web.go | 4 - {modules => services}/activitypub/client.go | 0 .../activitypub/client_test.go | 0 services/activitypub/follow.go | 53 +++++ {modules => services}/activitypub/iri.go | 0 {modules => services}/activitypub/keypair.go | 0 .../activitypub/keypair_test.go | 0 .../activitypub/main_test.go | 0 .../activitypub/transport.go | 0 .../activitypub/user_settings.go | 0 .../activitypub/user_settings_test.go | 0 .../repository/activitypub.go | 39 +--- services/user/activitypub.go | 145 ------------- services/user/user.go | 44 ++++ .../api_activitypub_person_test.go | 2 +- 29 files changed, 392 insertions(+), 376 deletions(-) delete mode 100644 modules/activitypub/comment.go delete mode 100644 modules/activitypub/issue.go delete mode 100644 modules/activitypub/repo.go rename routers/{web => api/v1/activitypub}/authorize_interaction.go (76%) create mode 100644 routers/api/v1/activitypub/create.go rename {modules => routers/api/v1}/activitypub/follow.go (65%) create mode 100644 routers/api/v1/activitypub/fork.go rename {modules => routers/api/v1}/activitypub/pull_request.go (90%) rename {modules => routers/api/v1}/activitypub/star.go (67%) rename {modules => services}/activitypub/client.go (100%) rename {modules => services}/activitypub/client_test.go (100%) create mode 100644 services/activitypub/follow.go rename {modules => services}/activitypub/iri.go (100%) rename {modules => services}/activitypub/keypair.go (100%) rename {modules => services}/activitypub/keypair_test.go (100%) rename {modules => services}/activitypub/main_test.go (100%) rename {modules => services}/activitypub/transport.go (100%) rename {modules => services}/activitypub/user_settings.go (100%) rename {modules => services}/activitypub/user_settings_test.go (100%) rename modules/activitypub/fork.go => services/repository/activitypub.go (56%) delete mode 100644 services/user/activitypub.go diff --git a/modules/activitypub/comment.go b/modules/activitypub/comment.go deleted file mode 100644 index 9052fa3600..0000000000 --- a/modules/activitypub/comment.go +++ /dev/null @@ -1,42 +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 activitypub - -import ( - "context" - - "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - - ap "github.com/go-ap/activitypub" -) - -// Create a comment -func Comment(ctx context.Context, note *ap.Note) error { - actorUser, err := PersonIRIToUser(ctx, note.AttributedTo.GetLink()) - if err != nil { - return err - } - - username, reponame, idx, err := TicketIRIToName(note.Context.GetLink()) - if err != nil { - return err - } - repo, err := repo_model.GetRepositoryByOwnerAndNameCtx(ctx, username, reponame) - if err != nil { - return err - } - issue, err := issues.GetIssueByIndex(repo.ID, idx) - if err != nil { - return err - } - _, err = issues.CreateCommentCtx(ctx, &issues.CreateCommentOptions{ - Doer: actorUser, - Repo: repo, - Issue: issue, - Content: note.Content.String(), - }) - return err -} diff --git a/modules/activitypub/issue.go b/modules/activitypub/issue.go deleted file mode 100644 index 720e6ba91e..0000000000 --- a/modules/activitypub/issue.go +++ /dev/null @@ -1,47 +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 activitypub - -import ( - "context" - "fmt" - "strconv" - - issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/modules/forgefed" - issue_service "code.gitea.io/gitea/services/issue" - - ap "github.com/go-ap/activitypub" -) - -// Create an issue -func ReceiveIssue(ctx context.Context, ticket *forgefed.Ticket) error { - // Construct issue - user, err := PersonIRIToUser(ctx, ap.IRI(ticket.AttributedTo.GetLink().String())) - if err != nil { - return err - } - repo, err := RepositoryIRIToRepository(ctx, ap.IRI(ticket.Context.GetLink().String())) - if err != nil { - return err - } - fmt.Println(ticket) - fmt.Println(ticket.Name.String()) - idx, err := strconv.ParseInt(ticket.Name.String()[1:], 10, 64) - if err != nil { - return err - } - issue := &issues_model.Issue{ - ID: idx, - RepoID: repo.ID, - Repo: repo, - Title: ticket.Summary.String(), - PosterID: user.ID, - Poster: user, - Content: ticket.Content.String(), - } - fmt.Println(issue) - return issue_service.NewIssue(repo, issue, nil, nil, nil) -} diff --git a/modules/activitypub/repo.go b/modules/activitypub/repo.go deleted file mode 100644 index 52c096a269..0000000000 --- a/modules/activitypub/repo.go +++ /dev/null @@ -1,44 +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 activitypub - -import ( - "context" - - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/forgefed" - repo_module "code.gitea.io/gitea/modules/repository" - repo_service "code.gitea.io/gitea/services/repository" -) - -// Create a new federated repo from a Repository object -func FederatedRepoNew(ctx context.Context, repository *forgefed.Repository) error { - user, err := PersonIRIToUser(ctx, repository.AttributedTo.GetLink()) - if err != nil { - return err - } - - _, err = repo_model.GetRepositoryByOwnerAndNameCtx(ctx, user.Name, repository.Name.String()) - if err == nil { - return nil - } - - repo, err := repo_service.CreateRepository(user, user, repo_module.CreateRepoOptions{ - Name: repository.Name.String(), - }) - if err != nil { - return err - } - - if repository.ForkedFrom != nil { - repo.IsFork = true - forkedFrom, err := RepositoryIRIToRepository(ctx, repository.ForkedFrom.GetLink()) - if err != nil { - return err - } - repo.ForkID = forkedFrom.ID - } - return nil -} diff --git a/routers/web/authorize_interaction.go b/routers/api/v1/activitypub/authorize_interaction.go similarity index 76% rename from routers/web/authorize_interaction.go rename to routers/api/v1/activitypub/authorize_interaction.go index 45c257e623..3e1a94d85a 100644 --- a/routers/web/authorize_interaction.go +++ b/routers/api/v1/activitypub/authorize_interaction.go @@ -2,17 +2,16 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package web +package activitypub import ( "net/http" "net/url" "strconv" - "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/forgefed" - user_service "code.gitea.io/gitea/services/user" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" ) @@ -45,7 +44,7 @@ func AuthorizeInteraction(ctx *context.Context) { ctx.ServerError("UnmarshalJSON", err) return } - err = user_service.FederatedUserNew(ctx, object.(*ap.Person)) + err = createPerson(ctx, object.(*ap.Person)) if err != nil { ctx.ServerError("FederatedUserNew", err) return @@ -59,29 +58,7 @@ func AuthorizeInteraction(ctx *context.Context) { case forgefed.RepositoryType: // Federated repository err = forgefed.OnRepository(object, func(r *forgefed.Repository) error { - ownerURL, err := url.Parse(r.AttributedTo.GetLink().String()) - if err != nil { - return err - } - // Fetch person object - resp, err := activitypub.Fetch(ownerURL) - if err != nil { - return err - } - // Parse person object - ap.ItemTyperFunc = forgefed.GetItemByType - ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn - ap.NotEmptyChecker = forgefed.NotEmpty - object, err := ap.UnmarshalJSON(resp) - if err != nil { - return err - } - // Create federated user - err = user_service.FederatedUserNew(ctx, object.(*ap.Person)) - if err != nil { - return err - } - return activitypub.FederatedRepoNew(ctx, r) + return createRepository(ctx, r) }) if err != nil { ctx.ServerError("FederatedRepoNew", err) @@ -117,12 +94,12 @@ func AuthorizeInteraction(ctx *context.Context) { } // Create federated repo err = forgefed.OnRepository(object, func(r *forgefed.Repository) error { - return activitypub.FederatedRepoNew(ctx, r) + return createRepository(ctx, r) }) if err != nil { return err } - return activitypub.ReceiveIssue(ctx, t) + return createIssue(ctx, t) }) if err != nil { ctx.ServerError("ReceiveIssue", err) diff --git a/routers/api/v1/activitypub/create.go b/routers/api/v1/activitypub/create.go new file mode 100644 index 0000000000..eb9b6244c4 --- /dev/null +++ b/routers/api/v1/activitypub/create.go @@ -0,0 +1,199 @@ +// 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 ( + "context" + "errors" + "net/url" + "strconv" + "strings" + + "code.gitea.io/gitea/models/auth" + issue_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/forgefed" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/activitypub" + issue_service "code.gitea.io/gitea/services/issue" + repo_service "code.gitea.io/gitea/services/repository" + user_service "code.gitea.io/gitea/services/user" + + ap "github.com/go-ap/activitypub" +) + +// Create a new federated user from a Person object +func createPerson(ctx context.Context, person *ap.Person) error { + name, err := activitypub.PersonIRIToName(person.GetLink()) + if err != nil { + return err + } + + exists, err := user_model.IsUserExist(ctx, 0, name) + if err != nil { + return err + } + if exists { + return nil + } + + var email string + if person.Location != nil { + email = person.Location.GetLink().String() + } else { + // This might not even work + email = strings.ReplaceAll(name, "@", "+") + "@" + setting.Service.NoReplyAddress + } + + if person.PublicKey.PublicKeyPem == "" { + return errors.New("person public key not found") + } + + user := &user_model.User{ + Name: name, + FullName: person.Name.String(), // May not exist!! + Email: email, + LoginType: auth.Federated, + LoginName: person.GetLink().String(), + } + err = user_model.CreateUser(user) + if err != nil { + return err + } + + if person.Icon != nil { + icon := person.Icon.(*ap.Image) + iconURL, err := icon.URL.GetLink().URL() + if err != nil { + return err + } + + body, err := activitypub.Fetch(iconURL) + if err != nil { + return err + } + + err = user_service.UploadAvatar(user, body) + if err != nil { + return err + } + } + + err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, "") + if err != nil { + return err + } + return user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, person.PublicKey.PublicKeyPem) +} + +// Create a new federated repo from a Repository object +func createRepository(ctx context.Context, repository *forgefed.Repository) error { + ownerURL, err := url.Parse(repository.AttributedTo.GetLink().String()) + if err != nil { + return err + } + // Fetch person object + resp, err := activitypub.Fetch(ownerURL) + if err != nil { + return err + } + // Parse person object + ap.ItemTyperFunc = forgefed.GetItemByType + ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn + ap.NotEmptyChecker = forgefed.NotEmpty + object, err := ap.UnmarshalJSON(resp) + if err != nil { + return err + } + // Create federated user + err = createPerson(ctx, object.(*ap.Person)) + if err != nil { + return err + } + + user, err := activitypub.PersonIRIToUser(ctx, repository.AttributedTo.GetLink()) + if err != nil { + return err + } + + _, err = repo_model.GetRepositoryByOwnerAndNameCtx(ctx, user.Name, repository.Name.String()) + if err == nil { + return nil + } + + repo, err := repo_service.CreateRepository(user, user, repo_module.CreateRepoOptions{ + Name: repository.Name.String(), + }) + if err != nil { + return err + } + + if repository.ForkedFrom != nil { + repo.IsFork = true + forkedFrom, err := activitypub.RepositoryIRIToRepository(ctx, repository.ForkedFrom.GetLink()) + if err != nil { + return err + } + repo.ForkID = forkedFrom.ID + } + return nil +} + +// Create an issue +func createIssue(ctx context.Context, ticket *forgefed.Ticket) error { + // Construct issue + user, err := activitypub.PersonIRIToUser(ctx, ap.IRI(ticket.AttributedTo.GetLink().String())) + if err != nil { + return err + } + repo, err := activitypub.RepositoryIRIToRepository(ctx, ap.IRI(ticket.Context.GetLink().String())) + if err != nil { + return err + } + idx, err := strconv.ParseInt(ticket.Name.String()[1:], 10, 64) + if err != nil { + return err + } + issue := &issue_model.Issue{ + ID: idx, + RepoID: repo.ID, + Repo: repo, + Title: ticket.Summary.String(), + PosterID: user.ID, + Poster: user, + Content: ticket.Content.String(), + } + return issue_service.NewIssue(repo, issue, nil, nil, nil) +} + +// Create a comment +func createComment(ctx context.Context, note *ap.Note) error { + actorUser, err := activitypub.PersonIRIToUser(ctx, note.AttributedTo.GetLink()) + if err != nil { + return err + } + + username, reponame, idx, err := activitypub.TicketIRIToName(note.Context.GetLink()) + if err != nil { + return err + } + repo, err := repo_model.GetRepositoryByOwnerAndNameCtx(ctx, username, reponame) + if err != nil { + return err + } + issue, err := issue_model.GetIssueByIndex(repo.ID, idx) + if err != nil { + return err + } + _, err = issue_model.CreateCommentCtx(ctx, &issue_model.CreateCommentOptions{ + Doer: actorUser, + Repo: repo, + Issue: issue, + Content: note.Content.String(), + }) + return err +} diff --git a/modules/activitypub/follow.go b/routers/api/v1/activitypub/follow.go similarity index 65% rename from modules/activitypub/follow.go rename to routers/api/v1/activitypub/follow.go index a36404810a..e2ebe14852 100644 --- a/modules/activitypub/follow.go +++ b/routers/api/v1/activitypub/follow.go @@ -9,22 +9,23 @@ import ( "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" ) -// Process a Follow activity -func Follow(ctx context.Context, follow ap.Follow) error { +// Process an incoming Follow activity +func follow(ctx context.Context, follow ap.Follow) error { // Actor is the user performing the follow actorIRI := follow.Actor.GetLink() - actorUser, err := PersonIRIToUser(ctx, actorIRI) + actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI) if err != nil { return err } // Object is the user being followed objectIRI := follow.Object.GetLink() - objectUser, err := PersonIRIToUser(ctx, objectIRI) + objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI) // Must be a local user if err != nil || strings.Contains(objectUser.Name, "@") { return err @@ -40,22 +41,27 @@ func Follow(ctx context.Context, follow ap.Follow) error { accept.Actor = ap.Person{ID: objectIRI} accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")} accept.Object = follow - return Send(objectUser, accept) + return activitypub.Send(objectUser, accept) } -// Process a Undo follow activity -func Unfollow(ctx context.Context, unfollow ap.Undo) error { - follow := unfollow.Object.(*ap.Follow) +// Process an incoming Undo follow activity +func unfollow(ctx context.Context, unfollow ap.Undo) error { + // Object contains the follow + follow, err := ap.To[ap.Follow](unfollow.Object) + if err != nil { + return err + } + // Actor is the user performing the undo follow actorIRI := follow.Actor.GetLink() - actorUser, err := PersonIRIToUser(ctx, actorIRI) + actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI) if err != nil { return err } // Object is the user being unfollowed objectIRI := follow.Object.GetLink() - objectUser, err := PersonIRIToUser(ctx, objectIRI) + objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI) // Must be a local user if err != nil || strings.Contains(objectUser.Name, "@") { return err diff --git a/routers/api/v1/activitypub/fork.go b/routers/api/v1/activitypub/fork.go new file mode 100644 index 0000000000..26a75c575c --- /dev/null +++ b/routers/api/v1/activitypub/fork.go @@ -0,0 +1,51 @@ +// 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 ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/forgefed" + "code.gitea.io/gitea/services/activitypub" + repo_service "code.gitea.io/gitea/services/repository" + + ap "github.com/go-ap/activitypub" +) + +func fork(ctx context.Context, create ap.Create) error { + // Object is the new fork repository + repository, err := ap.To[forgefed.Repository](create.Object) + if err != nil { + return nil + } + + // TODO: Clean this up + actor, err := activitypub.PersonIRIToUser(ctx, create.Actor.GetLink()) + if err != nil { + return err + } + + // Don't create an actual copy of the remote repo! + // https://gitea.com/xy/gitea/issues/7 + + // Create the fork + repoIRI := repository.GetLink() + username, reponame, err := activitypub.RepositoryIRIToName(repoIRI) + if err != nil { + return err + } + + // FederatedUserNew(username + "@" + instance, ) + user, _ := user_model.GetUserByName(ctx, username) + + // var repo forgefed.Repository + // repo = activity.Object + repo, _ := repo_model.GetRepositoryByOwnerAndName(actor.Name, reponame) // hardcoded for now :( + + _, err = repo_service.ForkRepository(ctx, user, user, repo_service.ForkRepoOptions{BaseRepo: repo, Name: reponame, Description: "this is a remote fork"}) + return err +} diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go index 348629873b..4fd7fa56ff 100644 --- a/routers/api/v1/activitypub/person.go +++ b/routers/api/v1/activitypub/person.go @@ -14,13 +14,13 @@ import ( "code.gitea.io/gitea/models/db" 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" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" ) @@ -131,9 +131,9 @@ func PersonInbox(ctx *context.APIContext) { // Process activity switch activity.Type { case ap.FollowType: - err = activitypub.Follow(ctx, activity) + err = follow(ctx, activity) case ap.UndoType: - err = activitypub.Unfollow(ctx, activity) + err = unfollow(ctx, activity) default: log.Info("Incoming unsupported ActivityStreams type: %s", activity.GetType()) ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") diff --git a/modules/activitypub/pull_request.go b/routers/api/v1/activitypub/pull_request.go similarity index 90% rename from modules/activitypub/pull_request.go rename to routers/api/v1/activitypub/pull_request.go index c6e86c482b..3fa0a1ba18 100644 --- a/modules/activitypub/pull_request.go +++ b/routers/api/v1/activitypub/pull_request.go @@ -12,13 +12,14 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/activitypub" pull_service "code.gitea.io/gitea/services/pull" ) -func PullRequest(ctx context.Context, ticket *forgefed.Ticket) error { +func createPullRequest(ctx context.Context, ticket *forgefed.Ticket) error { // TODO: Clean this up - actorUser, err := PersonIRIToUser(ctx, ticket.AttributedTo.GetLink()) + actorUser, err := activitypub.PersonIRIToUser(ctx, ticket.AttributedTo.GetLink()) if err != nil { log.Warn("Couldn't find ticket actor user", err) } diff --git a/routers/api/v1/activitypub/repo.go b/routers/api/v1/activitypub/repo.go index 7ae0b636a8..c948fe40e2 100644 --- a/routers/api/v1/activitypub/repo.go +++ b/routers/api/v1/activitypub/repo.go @@ -9,7 +9,6 @@ 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" @@ -128,25 +127,25 @@ func RepoInbox(ctx *context.APIContext) { switch o.Type { case forgefed.RepositoryType: // Fork created by remote instance - return activitypub.ReceiveFork(ctx, activity) + return fork(ctx, activity) case forgefed.TicketType: // New issue or pull request return forgefed.OnTicket(o, func(t *forgefed.Ticket) error { if t.Origin != nil { // New pull request - return activitypub.PullRequest(ctx, t) + return createPullRequest(ctx, t) } // New issue - return activitypub.ReceiveIssue(ctx, t) + return createIssue(ctx, t) }) case ap.NoteType: // New comment - return activitypub.Comment(ctx, o) + return createComment(ctx, o) } return nil }) case ap.LikeType: - err = activitypub.ReceiveStar(ctx, activity) + err = star(ctx, activity) default: log.Info("Incoming unsupported ActivityStreams type: %s", activity.Type) ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 7c66cebbd0..8e48263774 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -12,10 +12,9 @@ import ( "net/http" "net/url" - "code.gitea.io/gitea/modules/activitypub" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" - user_service "code.gitea.io/gitea/services/user" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" "github.com/go-fed/httpsig" @@ -85,7 +84,7 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er return } - err = user_service.FederatedUserNew(ctx, &person) + err = createPerson(ctx, &person) return authenticated, err } diff --git a/routers/api/v1/activitypub/response.go b/routers/api/v1/activitypub/response.go index bec876d6d9..9e5c9f4d19 100644 --- a/routers/api/v1/activitypub/response.go +++ b/routers/api/v1/activitypub/response.go @@ -7,10 +7,10 @@ package activitypub import ( "net/http" - "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/services/activitypub" ap "github.com/go-ap/activitypub" "github.com/go-ap/jsonld" diff --git a/modules/activitypub/star.go b/routers/api/v1/activitypub/star.go similarity index 67% rename from modules/activitypub/star.go rename to routers/api/v1/activitypub/star.go index 3b85ca5a1f..df58f77f1a 100644 --- a/modules/activitypub/star.go +++ b/routers/api/v1/activitypub/star.go @@ -9,17 +9,18 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" ) // Process a Like activity to star a repository -func ReceiveStar(ctx context.Context, like ap.Like) (err error) { - user, err := PersonIRIToUser(ctx, like.Actor.GetLink()) +func star(ctx context.Context, like ap.Like) (err error) { + user, err := activitypub.PersonIRIToUser(ctx, like.Actor.GetLink()) if err != nil { return } - repo, err := RepositoryIRIToRepository(ctx, like.Object.GetLink()) + repo, err := activitypub.RepositoryIRIToRepository(ctx, like.Object.GetLink()) if err != nil || strings.Contains(repo.Name, "@") || repo.IsPrivate { return } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 65b01928c5..06c80f789b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -644,6 +644,7 @@ func Routes(ctx gocontext.Context) *web.Route { } m.Get("/version", misc.Version) if setting.Federation.Enabled { + m.Get("/authorize_interaction", activitypub.AuthorizeInteraction) m.Get("/nodeinfo", misc.NodeInfo) m.Group("/activitypub", func() { m.Group("/user/{username}", func() { diff --git a/routers/web/web.go b/routers/web/web.go index ae11e916fd..48b33813c9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1323,10 +1323,6 @@ func RegisterRoutes(m *web.Route) { m.Get("/new", user.NewAvailable) }, reqSignIn) - if setting.Federation.Enabled { - m.Get("/authorize_interaction", AuthorizeInteraction) - } - if setting.API.EnableSwagger { m.Get("/swagger.v1.json", SwaggerV1Json) } diff --git a/modules/activitypub/client.go b/services/activitypub/client.go similarity index 100% rename from modules/activitypub/client.go rename to services/activitypub/client.go diff --git a/modules/activitypub/client_test.go b/services/activitypub/client_test.go similarity index 100% rename from modules/activitypub/client_test.go rename to services/activitypub/client_test.go diff --git a/services/activitypub/follow.go b/services/activitypub/follow.go new file mode 100644 index 0000000000..1dc32a1252 --- /dev/null +++ b/services/activitypub/follow.go @@ -0,0 +1,53 @@ +// 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 ( + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + ap "github.com/go-ap/activitypub" +) + +// Create and send Follow activity +func Follow(userID, followID int64) error { + followUser, err := user_model.GetUserByID(followID) + if err != nil { + return err + } + + actorUser, err := user_model.GetUserByID(userID) + if err != nil { + return err + } + + object := ap.PersonNew(ap.IRI(followUser.LoginName)) + follow := ap.FollowNew("", object) + follow.Type = ap.FollowType + follow.Actor = ap.PersonNew(ap.IRI(setting.AppURL + "api/v1/activitypub/user/" + actorUser.Name)) + follow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} + return Send(actorUser, follow) +} + +// Create and send Undo Follow activity +func Unfollow(userID, followID int64) error { + followUser, err := user_model.GetUserByID(followID) + if err != nil { + return err + } + + actorUser, err := user_model.GetUserByID(userID) + if err != nil { + return err + } + + object := ap.PersonNew(ap.IRI(followUser.LoginName)) + follow := ap.FollowNew("", object) + follow.Actor = ap.PersonNew(ap.IRI(setting.AppURL + "api/v1/activitypub/user/" + actorUser.Name)) + unfollow := ap.UndoNew("", follow) + unfollow.Type = ap.UndoType + unfollow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} + return Send(actorUser, unfollow) +} diff --git a/modules/activitypub/iri.go b/services/activitypub/iri.go similarity index 100% rename from modules/activitypub/iri.go rename to services/activitypub/iri.go diff --git a/modules/activitypub/keypair.go b/services/activitypub/keypair.go similarity index 100% rename from modules/activitypub/keypair.go rename to services/activitypub/keypair.go diff --git a/modules/activitypub/keypair_test.go b/services/activitypub/keypair_test.go similarity index 100% rename from modules/activitypub/keypair_test.go rename to services/activitypub/keypair_test.go diff --git a/modules/activitypub/main_test.go b/services/activitypub/main_test.go similarity index 100% rename from modules/activitypub/main_test.go rename to services/activitypub/main_test.go diff --git a/modules/activitypub/transport.go b/services/activitypub/transport.go similarity index 100% rename from modules/activitypub/transport.go rename to services/activitypub/transport.go diff --git a/modules/activitypub/user_settings.go b/services/activitypub/user_settings.go similarity index 100% rename from modules/activitypub/user_settings.go rename to services/activitypub/user_settings.go diff --git a/modules/activitypub/user_settings_test.go b/services/activitypub/user_settings_test.go similarity index 100% rename from modules/activitypub/user_settings_test.go rename to services/activitypub/user_settings_test.go diff --git a/modules/activitypub/fork.go b/services/repository/activitypub.go similarity index 56% rename from modules/activitypub/fork.go rename to services/repository/activitypub.go index bf41a342a4..bb22f5b1f3 100644 --- a/modules/activitypub/fork.go +++ b/services/repository/activitypub.go @@ -2,17 +2,15 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package activitypub +package repository import ( "context" - repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/activitypub" "code.gitea.io/gitea/services/migrations" - repo_service "code.gitea.io/gitea/services/repository" ap "github.com/go-ap/activitypub" ) @@ -44,36 +42,5 @@ func CreateFork(ctx context.Context, instance, username, reponame, destUsername // repo.ForkedFrom = forgefed.RepositoryNew(ap.IRI()) create.Object = repo - return Send(user, &create) -} - -func ReceiveFork(ctx context.Context, create ap.Create) error { - // TODO: Clean this up - - repository := create.Object.(*forgefed.Repository) - - actor, err := PersonIRIToUser(ctx, create.Actor.GetLink()) - if err != nil { - return err - } - - // Don't create an actual copy of the remote repo! - // https://gitea.com/xy/gitea/issues/7 - - // Create the fork - repoIRI := repository.GetLink() - username, reponame, err := RepositoryIRIToName(repoIRI) - if err != nil { - return err - } - - // FederatedUserNew(username + "@" + instance, ) - user, _ := user_model.GetUserByName(ctx, username) - - // var repo forgefed.Repository - // repo = activity.Object - repo, _ := repo_model.GetRepositoryByOwnerAndName(actor.Name, reponame) // hardcoded for now :( - - _, err = repo_service.ForkRepository(ctx, user, user, repo_service.ForkRepoOptions{BaseRepo: repo, Name: reponame, Description: "this is a remote fork"}) - return err + return activitypub.Send(user, &create) } diff --git a/services/user/activitypub.go b/services/user/activitypub.go deleted file mode 100644 index 8b80a42bf7..0000000000 --- a/services/user/activitypub.go +++ /dev/null @@ -1,145 +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 user - -import ( - "context" - "errors" - "strings" - - "code.gitea.io/gitea/models/auth" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/activitypub" - "code.gitea.io/gitea/modules/setting" - - ap "github.com/go-ap/activitypub" -) - -// FollowUser marks someone be another's follower. -func FollowUser(userID, followID int64) (err error) { - if userID == followID || user_model.IsFollowing(userID, followID) { - return nil - } - - followUser, err := user_model.GetUserByID(followID) - if err != nil { - return err - } - if followUser.LoginType == auth.Federated { - // Following remote user - actorUser, err := user_model.GetUserByID(userID) - if err != nil { - return err - } - - object := ap.PersonNew(ap.IRI(followUser.LoginName)) - follow := ap.FollowNew("", object) - follow.Type = ap.FollowType - follow.Actor = ap.PersonNew(ap.IRI(setting.AppURL + "api/v1/activitypub/user/" + actorUser.Name)) - follow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} - err = activitypub.Send(actorUser, follow) - if err != nil { - return err - } - } - - return user_model.FollowUser(userID, followID) -} - -// UnfollowUser unmarks someone as another's follower. -func UnfollowUser(userID, followID int64) (err error) { - if userID == followID || !user_model.IsFollowing(userID, followID) { - return nil - } - - followUser, err := user_model.GetUserByID(followID) - if err != nil { - return err - } - if followUser.LoginType == auth.Federated { - // Unfollowing remote user - actorUser, err := user_model.GetUserByID(userID) - if err != nil { - return err - } - - object := ap.PersonNew(ap.IRI(followUser.LoginName)) - follow := ap.FollowNew("", object) - follow.Actor = ap.PersonNew(ap.IRI(setting.AppURL + "api/v1/activitypub/user/" + actorUser.Name)) - unfollow := ap.UndoNew("", follow) - unfollow.Type = ap.UndoType - unfollow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} - err = activitypub.Send(actorUser, unfollow) - if err != nil { - return err - } - } - - return user_model.UnfollowUser(userID, followID) -} - -// Create a new federated user from a Person object -func FederatedUserNew(ctx context.Context, person *ap.Person) error { - name, err := activitypub.PersonIRIToName(person.GetLink()) - if err != nil { - return err - } - - exists, err := user_model.IsUserExist(ctx, 0, name) - if err != nil { - return err - } - if exists { - return nil - } - - var email string - if person.Location != nil { - email = person.Location.GetLink().String() - } else { - // This might not even work - email = strings.ReplaceAll(name, "@", "+") + "@" + setting.Service.NoReplyAddress - } - - if person.PublicKey.PublicKeyPem == "" { - return errors.New("person public key not found") - } - - user := &user_model.User{ - Name: name, - FullName: person.Name.String(), // May not exist!! - Email: email, - LoginType: auth.Federated, - LoginName: person.GetLink().String(), - } - err = user_model.CreateUser(user) - if err != nil { - return err - } - - if person.Icon != nil { - icon := person.Icon.(*ap.Image) - iconURL, err := icon.URL.GetLink().URL() - if err != nil { - return err - } - - body, err := activitypub.Fetch(iconURL) - if err != nil { - return err - } - - err = UploadAvatar(user, body) - if err != nil { - return err - } - } - - err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, "") - if err != nil { - return err - } - return user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, person.PublicKey.PublicKeyPem) -} diff --git a/services/user/user.go b/services/user/user.go index c8b497a5c4..139f7c7602 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" @@ -26,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/activitypub" "code.gitea.io/gitea/services/packages" ) @@ -280,3 +282,45 @@ func DeleteAvatar(u *user_model.User) error { } return nil } + +// FollowUser marks someone be another's follower. +func FollowUser(userID, followID int64) (err error) { + if userID == followID || user_model.IsFollowing(userID, followID) { + return nil + } + + followUser, err := user_model.GetUserByID(followID) + if err != nil { + return + } + if followUser.LoginType == auth.Federated { + // Following remote user + err = activitypub.Follow(userID, followID) + if err != nil { + return + } + } + + return user_model.FollowUser(userID, followID) +} + +// UnfollowUser unmarks someone as another's follower. +func UnfollowUser(userID, followID int64) (err error) { + if userID == followID || !user_model.IsFollowing(userID, followID) { + return nil + } + + followUser, err := user_model.GetUserByID(followID) + if err != nil { + return + } + if followUser.LoginType == auth.Federated { + // Unfollowing remote user + err = activitypub.Unfollow(userID, followID) + if err != nil { + return + } + } + + return user_model.UnfollowUser(userID, followID) +} diff --git a/tests/integration/api_activitypub_person_test.go b/tests/integration/api_activitypub_person_test.go index 88ce8f3eaa..0765a46455 100644 --- a/tests/integration/api_activitypub_person_test.go +++ b/tests/integration/api_activitypub_person_test.go @@ -13,9 +13,9 @@ import ( "testing" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" + "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" "github.com/stretchr/testify/assert"