diff --git a/models/db/name.go b/models/db/name.go index ae1876a5f1..c9dcdd7f39 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -17,7 +17,7 @@ var ( ErrNameEmpty = errors.New("Name is empty") // AlphaDashDotPattern characters prohibited in a user name (anything except A-Za-z0-9_.-) - AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.@]`) + AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.@]`) // Ugly hack to allow remote usernames to contain @ ) // ErrNameReserved represents a "reserved name" error. diff --git a/models/forgefed/forgefed.go b/models/forgefed/forgefed.go index 73767b82d9..fcc9b53fac 100644 --- a/models/forgefed/forgefed.go +++ b/models/forgefed/forgefed.go @@ -8,6 +8,8 @@ import ( ap "github.com/go-ap/activitypub" ) +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) { diff --git a/models/forgefed/ticket.go b/models/forgefed/ticket.go index fa988f5d41..322cf87b5a 100644 --- a/models/forgefed/ticket.go +++ b/models/forgefed/ticket.go @@ -27,6 +27,10 @@ type Ticket struct { ResolvedBy ap.Item `jsonld:"resolvedBy,omitempty"` // Resolved When the ticket has been marked as resolved Resolved time.Time `jsonld:"resolved,omitempty"` + // Origin The head branch if this ticket is a pull request + Origin ap.Item `jsonld:"origin,omitempty"` + // Target The base branch if this ticket is a pull request + Target ap.Item `jsonld:"target,omitempty"` } // TicketNew initializes a Ticket type Object @@ -56,6 +60,12 @@ func (t Ticket) MarshalJSON() ([]byte, error) { if !t.Resolved.IsZero() { ap.WriteTimeJSONProp(&b, "resolved", t.Resolved) } + if t.Origin != nil { + ap.WriteItemJSONProp(&b, "origin", t.Origin) + } + if t.Target != nil { + ap.WriteItemJSONProp(&b, "target", t.Target) + } ap.Write(&b, '}') return b, nil } @@ -72,6 +82,8 @@ func (t *Ticket) UnmarshalJSON(data []byte) error { t.IsResolved = ap.JSONGetBoolean(val, "isResolved") t.ResolvedBy = ap.JSONGetItem(val, "resolvedBy") t.Resolved = ap.JSONGetTime(val, "resolved") + t.Origin = ap.JSONGetItem(val, "origin") + t.Target = ap.JSONGetItem(val, "target") return ap.OnObject(&t.Object, func(a *ap.Object) error { return ap.LoadObject(val, a) diff --git a/modules/activitypub/comment.go b/modules/activitypub/comment.go new file mode 100644 index 0000000000..b210de2525 --- /dev/null +++ b/modules/activitypub/comment.go @@ -0,0 +1,41 @@ +// 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" + "strconv" + "strings" + + "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/log" + + ap "github.com/go-ap/activitypub" +) + +// Create a comment +func Comment(ctx context.Context, note ap.Note) { + actorUser, err := personIRIToUser(ctx, note.AttributedTo.GetLink()) + if err != nil { + log.Warn("Couldn't find actor", err) + } + + // TODO: Move IRI processing stuff to iri.go + context := note.Context.GetLink() + contextSplit := strings.Split(context.String(), "/") + username := contextSplit[3] + reponame := contextSplit[4] + repo, _ := repo_model.GetRepositoryByOwnerAndName(username, reponame) + + idx, _ := strconv.ParseInt(contextSplit[len(contextSplit)-1], 10, 64) + issue, _ := issues.GetIssueByIndex(repo.ID, idx) + issues.CreateCommentCtx(ctx, &issues.CreateCommentOptions{ + Doer: actorUser, + Repo: repo, + Issue: issue, + Content: note.Content.String(), + }) +} diff --git a/modules/activitypub/follow.go b/modules/activitypub/follow.go index 919b697a8f..c0ccf6600a 100644 --- a/modules/activitypub/follow.go +++ b/modules/activitypub/follow.go @@ -14,33 +14,61 @@ import ( ap "github.com/go-ap/activitypub" ) -func Follow(ctx context.Context, activity ap.Follow) { - actorIRI := activity.Actor.GetID() - objectIRI := activity.Object.GetID() - actorIRISplit := strings.Split(actorIRI.String(), "/") - objectIRISplit := strings.Split(objectIRI.String(), "/") - actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2] - objectName := objectIRISplit[len(objectIRISplit)-1] +// Process a Follow activity +func Follow(ctx context.Context, follow ap.Follow) { + // Actor is the user performing the follow + actorIRI := follow.Actor.GetID() + actorUser, err := personIRIToUser(ctx, actorIRI) + if err != nil { + log.Warn("Couldn't find actor user for follow", err) + return + } - err := FederatedUserNew(actorName, actorIRI) - if err != nil { - log.Warn("Couldn't create new user", err) - } - actorUser, err := user_model.GetUserByName(ctx, actorName) - if err != nil { - log.Warn("Couldn't find actor", err) - } - objectUser, err := user_model.GetUserByName(ctx, objectName) - if err != nil { - log.Warn("Couldn't find object", err) + // Object is the user being followed + objectIRI := follow.Object.GetID() + objectUser, err := personIRIToUser(ctx, objectIRI) + // Must be a local user + if strings.Contains(objectUser.Name, "@") || err != nil { + log.Warn("Couldn't find object user for follow", err) + return } user_model.FollowUser(actorUser.ID, objectUser.ID) - accept := ap.AcceptNew(objectIRI, activity) + // Send back an Accept activity + accept := ap.AcceptNew(objectIRI, follow) accept.Actor = ap.Person{ID: objectIRI} accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")} - accept.Object = activity - + accept.Object = follow + Send(objectUser, accept) +} + +// Process a Undo follow activity +// I haven't tried this yet so hopefully it works +func Unfollow(ctx context.Context, unfollow ap.Undo) { + // Actor is the user performing the undo follow + actorIRI := unfollow.Actor.GetID() + actorUser, err := personIRIToUser(ctx, actorIRI) + if err != nil { + log.Warn("Couldn't find actor user for follow", err) + return + } + + // Object is the user being unfollowed + objectIRI := unfollow.Object.GetID() + objectUser, err := personIRIToUser(ctx, objectIRI) + // Must be a local user + if strings.Contains(objectUser.Name, "@") || err != nil { + log.Warn("Couldn't find object user for follow", err) + return + } + + user_model.UnfollowUser(actorUser.ID, objectUser.ID) + + // Send back an Accept activity + accept := ap.AcceptNew(objectIRI, unfollow) + accept.Actor = ap.Person{ID: objectIRI} + accept.To = ap.ItemCollection{ap.IRI(actorIRI.String() + "/inbox")} + accept.Object = unfollow Send(objectUser, accept) } diff --git a/modules/activitypub/fork.go b/modules/activitypub/fork.go index dd67fa2899..e848e3f52a 100644 --- a/modules/activitypub/fork.go +++ b/modules/activitypub/fork.go @@ -8,7 +8,7 @@ import ( "context" "strings" - //"code.gitea.io/gitea/models/forgefed" + "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/log" @@ -20,6 +20,8 @@ import ( ) func Fork(ctx context.Context, instance, username, reponame, destUsername string) { + // TODO: Clean this up + // Migrate repository code user, _ := user_model.GetUserByName(ctx, destUsername) _, err := migrations.MigrateRepository(ctx, user, destUsername, migrations.MigrateOptions{ @@ -30,25 +32,27 @@ func Fork(ctx context.Context, instance, username, reponame, destUsername string log.Warn("Couldn't create fork", err) } - // Make the migrated repo a fork + // TODO: Make the migrated repo a fork // Send a Create activity to the instance we are forking from create := ap.Create{Type: ap.CreateType} create.To = ap.ItemCollection{ap.IRI("https://" + instance + "/api/v1/activitypub/repo/" + username + "/" + reponame + "/inbox")} - repo := ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + destUsername + "/" + reponame) - // repo := forgefed.RepositoryNew(ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + destUsername + "/" + reponame)) + repo := ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + destUsername + "/" + reponame) + // repo := forgefed.RepositoryNew(ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + destUsername + "/" + reponame)) // repo.ForkedFrom = forgefed.RepositoryNew(ap.IRI()) create.Object = repo Send(user, &create) } -func ForkFromCreate(ctx context.Context, activity ap.Create) { +func ForkFromCreate(ctx context.Context, repository forgefed.Repository) { + // TODO: Clean this up + // Don't create an actual copy of the remote repo! // https://gitea.com/Ta180m/gitea/issues/7 // Create the fork - repoIRI := activity.Object.GetID() + repoIRI := repository.GetID() repoIRISplit := strings.Split(repoIRI.String(), "/") instance := repoIRISplit[2] username := repoIRISplit[7] @@ -63,6 +67,4 @@ func ForkFromCreate(ctx context.Context, activity ap.Create) { _, err := repo_service.ForkRepository(ctx, user, user, repo_service.ForkRepoOptions{BaseRepo: repo, Name: reponame, Description: "this is a remote fork"}) log.Warn("Couldn't create copy of remote fork", err) - - // TODO: send back accept } diff --git a/modules/activitypub/iri.go b/modules/activitypub/iri.go new file mode 100644 index 0000000000..eb508a3433 --- /dev/null +++ b/modules/activitypub/iri.go @@ -0,0 +1,80 @@ +// 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" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + ap "github.com/go-ap/activitypub" +) + +// Returns the username corresponding to a Person actor IRI +func personIRIToName(personIRI ap.IRI) (string, error) { + personIRISplit := strings.Split(personIRI.String(), "/") + if len(personIRISplit) < 3 { + return "", errors.New("Not a Person actor IRI") + } + + instance := personIRISplit[2] + name := personIRISplit[len(personIRISplit)-1] + if instance == setting.Domain { + // Local user + return name, nil + } else { + // Remote user + // Get name in username@instance.com format + return name + "@" + instance, nil + } +} + +// Returns the user corresponding to a Person actor IRI +func personIRIToUser(ctx context.Context, personIRI ap.IRI) (*user_model.User, error) { + name, err := personIRIToName(personIRI) + if err != nil { + return nil, err + } + + user, err := user_model.GetUserByName(ctx, name) + if err != nil || !strings.Contains(name, "@") { + return user, err + } + FederatedUserNew(personIRI) + return user_model.GetUserByName(ctx, name) +} + +// Returns the owner and name corresponding to a Repository actor IRI +func repositoryIRIToName(repoIRI ap.IRI) (string, string, error) { + repoIRISplit := strings.Split(repoIRI.String(), "/") + if len(repoIRISplit) < 5 { + return "", "", errors.New("Not a Repository actor IRI") + } + + instance := repoIRISplit[2] + username := repoIRISplit[len(repoIRISplit)-2] + reponame := repoIRISplit[len(repoIRISplit)-1] + if instance == setting.Domain { + // Local repo + return username, reponame, nil + } else { + // Remote repo + return username + "@" + instance, reponame, nil + } +} + +// Returns the repository corresponding to a Repository actor IRI +func repositoryIRIToRepository(ctx context.Context, repoIRI ap.IRI) (*repo_model.Repository, error) { + username, reponame, err := repositoryIRIToName(repoIRI) + if err != nil { + return nil, err + } + + return repo_model.GetRepositoryByOwnerAndName(username, reponame) +} diff --git a/modules/activitypub/issue.go b/modules/activitypub/issue.go index e962d97544..3f875155b5 100644 --- a/modules/activitypub/issue.go +++ b/modules/activitypub/issue.go @@ -6,41 +6,11 @@ package activitypub import ( "context" - "strconv" - "strings" - "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/log" - - ap "github.com/go-ap/activitypub" + "code.gitea.io/gitea/models/forgefed" ) -func Comment(ctx context.Context, activity ap.Note) { - actorIRI := activity.AttributedTo.GetLink() - actorIRISplit := strings.Split(actorIRI.String(), "/") - actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2] - err := FederatedUserNew(actorName, actorIRI) - if err != nil { - log.Warn("Couldn't create new user", err) - } - actorUser, err := user_model.GetUserByName(ctx, actorName) - if err != nil { - log.Warn("Couldn't find actor", err) - } - - context := activity.Context.GetLink() - contextSplit := strings.Split(context.String(), "/") - username := contextSplit[3] - reponame := contextSplit[4] - repo, _ := repo_model.GetRepositoryByOwnerAndName(username, reponame) - idx, _ := strconv.ParseInt(contextSplit[len(contextSplit)-1], 10, 64) - issue, _ := issues.GetIssueByIndex(repo.ID, idx) - issues.CreateCommentCtx(ctx, &issues.CreateCommentOptions{ - Doer: actorUser, - Repo: repo, - Issue: issue, - Content: activity.Content.String(), - }) +// Create an issue +func Issue(ctx context.Context, ticket forgefed.Ticket) { + // TODO } diff --git a/modules/activitypub/pull_request.go b/modules/activitypub/pull_request.go index 10a3d3020f..2e1f61c073 100644 --- a/modules/activitypub/pull_request.go +++ b/modules/activitypub/pull_request.go @@ -9,31 +9,23 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models/forgefed" issues_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/log" pull_service "code.gitea.io/gitea/services/pull" - - ap "github.com/go-ap/activitypub" ) -func PullRequest(ctx context.Context, activity ap.Move) { - actorIRI := activity.AttributedTo.GetLink() - actorIRISplit := strings.Split(actorIRI.String(), "/") - actorName := actorIRISplit[len(actorIRISplit)-1] + "@" + actorIRISplit[2] - err := FederatedUserNew(actorName, actorIRI) +func PullRequest(ctx context.Context, ticket forgefed.Ticket) { + // TODO: Clean this up + + actorUser, err := personIRIToUser(ctx, ticket.AttributedTo.GetLink()) if err != nil { - log.Warn("Couldn't create new user", err) - } - actorUser, err := user_model.GetUserByName(ctx, actorName) - if err != nil { - log.Warn("Couldn't find actor", err) + log.Warn("Couldn't find ticket actor user", err) } - // This code is really messy - // The IRI processing stuff should be in a separate function - originIRI := activity.Origin.GetLink() + // TODO: The IRI processing stuff should be moved to iri.go + originIRI := ticket.Origin.GetLink() originIRISplit := strings.Split(originIRI.String(), "/") originInstance := originIRISplit[2] originUsername := originIRISplit[3] @@ -41,7 +33,7 @@ func PullRequest(ctx context.Context, activity ap.Move) { originBranch := originIRISplit[len(originIRISplit)-1] originRepo, _ := repo_model.GetRepositoryByOwnerAndName(originUsername+"@"+originInstance, originReponame) - targetIRI := activity.Target.GetLink() + targetIRI := ticket.Target.GetLink() targetIRISplit := strings.Split(targetIRI.String(), "/") // targetInstance := targetIRISplit[2] targetUsername := targetIRISplit[3] @@ -56,19 +48,18 @@ func PullRequest(ctx context.Context, activity ap.Move) { PosterID: actorUser.ID, Poster: actorUser, IsPull: true, - Content: "🎉", + Content: "🎉", // TODO: Get content from Ticket object } pr := &issues_model.PullRequest{ - HeadRepoID: originRepo.ID, - BaseRepoID: targetRepo.ID, - HeadBranch: originBranch, - HeadCommitID: "73f228996f27fad2c7bb60435f912d943b66b0ee", // hardcoded for now - BaseBranch: targetBranch, - HeadRepo: originRepo, - BaseRepo: targetRepo, - MergeBase: "", - Type: issues_model.PullRequestGitea, + HeadRepoID: originRepo.ID, + BaseRepoID: targetRepo.ID, + HeadBranch: originBranch, + BaseBranch: targetBranch, + HeadRepo: originRepo, + BaseRepo: targetRepo, + MergeBase: "", + Type: issues_model.PullRequestGitea, } err = pull_service.NewPullRequest(ctx, targetRepo, prIssue, []int64{}, []string{}, pr, []int64{}) diff --git a/modules/activitypub/send.go b/modules/activitypub/transport.go similarity index 77% rename from modules/activitypub/send.go rename to modules/activitypub/transport.go index ead664fd14..21d2fcf5de 100644 --- a/modules/activitypub/send.go +++ b/modules/activitypub/transport.go @@ -10,15 +10,17 @@ import ( "net/http" "net/url" + "code.gitea.io/gitea/models/forgefed" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" + "github.com/go-ap/jsonld" ) +// Fetch a remote ActivityStreams object func Fetch(iri *url.URL) (b []byte, err error) { req := httplib.NewRequest(iri.String(), http.MethodGet) req.Header("Accept", ActivityStreamsContentType) @@ -37,22 +39,21 @@ func Fetch(iri *url.URL) (b []byte, err error) { return b, err } +// Send an activity func Send(user *user_model.User, activity *ap.Activity) { - body, err := activity.MarshalJSON() + binary, err := jsonld.WithContext( + jsonld.IRI(ap.ActivityBaseURI), + jsonld.IRI(ap.SecurityContextURI), + jsonld.IRI(forgefed.ForgeFedNamespaceURI), + ).Marshal(activity) if err != nil { + log.Warn("Marshal", err) return } - var jsonmap map[string]interface{} - err = json.Unmarshal(body, &jsonmap) - if err != nil { - return - } - jsonmap["@context"] = "https://www.w3.org/ns/activitystreams" - body, _ = json.Marshal(jsonmap) for _, to := range activity.To { client, _ := NewClient(user, setting.AppURL+"api/v1/activitypub/user/"+user.Name+"#main-key") - resp, _ := client.Post(body, to.GetID().String()) + resp, _ := client.Post(binary, to.GetID().String()) respBody, _ := io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) log.Debug(string(respBody)) } diff --git a/modules/activitypub/user.go b/modules/activitypub/user.go index 4b7d958b56..592649645b 100644 --- a/modules/activitypub/user.go +++ b/modules/activitypub/user.go @@ -11,10 +11,15 @@ import ( ap "github.com/go-ap/activitypub" ) -func FederatedUserNew(name string, IRI ap.IRI) error { +func FederatedUserNew(IRI ap.IRI) error { + name, err := personIRIToName(IRI) + if err != nil { + return err + } + user := &user_model.User{ Name: name, - Email: name, // TODO: change this to something else to prevent collisions with normal users + Email: name, // TODO: change this to something else to prevent collisions with normal users, maybe fetch email using Gitea API LoginType: auth.Federated, Website: IRI.String(), } diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go index 03e9a7c597..c5884c9643 100644 --- a/routers/api/v1/activitypub/person.go +++ b/routers/api/v1/activitypub/person.go @@ -7,7 +7,6 @@ package activitypub import ( "io" "net/http" - "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" @@ -39,7 +38,7 @@ func Person(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name person := ap.PersonNew(ap.IRI(link)) person.Name = ap.NaturalLanguageValuesNew() @@ -66,15 +65,12 @@ func Person(ctx *context.APIContext) { person.Inbox = ap.IRI(link + "/inbox") person.Outbox = ap.IRI(link + "/outbox") - person.Following = ap.IRI(link + "/following") person.Followers = ap.IRI(link + "/followers") - person.Liked = ap.IRI(link + "/liked") person.PublicKey.ID = ap.IRI(link + "#main-key") person.PublicKey.Owner = ap.IRI(link) - publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser) if err != nil { ctx.ServerError("GetPublicKey", err) @@ -109,10 +105,13 @@ func PersonInbox(ctx *context.APIContext) { var activity ap.Activity activity.UnmarshalJSON(body) - if activity.Type == ap.FollowType { + switch activity.Type { + case ap.FollowType: activitypub.Follow(ctx, activity) - } else { - log.Warn("ActivityStreams type not supported", activity) + case ap.UndoType: + activitypub.Unfollow(ctx, activity) + default: + log.Debug("ActivityStreams type not supported", activity) ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") return } @@ -137,7 +136,7 @@ func PersonOutbox(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name feed, err := models.GetFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctx.ContextUser, @@ -186,7 +185,7 @@ func PersonFollowing(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name users, _, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { @@ -223,7 +222,7 @@ func PersonFollowers(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name users, _, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { @@ -235,6 +234,7 @@ func PersonFollowers(ctx *context.APIContext) { followers.TotalItems = uint(len(users)) for _, user := range users { + // TODO: handle non-Federated users person := ap.PersonNew(ap.IRI(user.Website)) followers.OrderedItems.Append(person) } @@ -259,7 +259,7 @@ func PersonLiked(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name + link := setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{ Actor: ctx.Doer, @@ -275,7 +275,8 @@ func PersonLiked(ctx *context.APIContext) { liked.TotalItems = uint(count) for _, repo := range repos { - repo := forgefed.RepositoryNew(ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + repo.OwnerName + "/" + repo.Name)) + // TODO: Handle remote starred repos + repo := forgefed.RepositoryNew(ap.IRI(setting.AppURL + "api/v1/activitypub/repo/" + repo.OwnerName + "/" + repo.Name)) liked.OrderedItems.Append(repo) } diff --git a/routers/api/v1/activitypub/repo.go b/routers/api/v1/activitypub/repo.go index 0f63e3e521..7060a63b95 100644 --- a/routers/api/v1/activitypub/repo.go +++ b/routers/api/v1/activitypub/repo.go @@ -7,21 +7,18 @@ package activitypub import ( "io" "net/http" - "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/forgefed" - 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/json" "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 +// Repo function returns the Repository actor of a repo func Repo(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/{reponame} activitypub activitypubRepo // --- @@ -43,7 +40,7 @@ func Repo(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name + link := setting.AppURL + "api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name repo := forgefed.RepositoryNew(ap.IRI(link)) repo.Name = ap.NaturalLanguageValuesNew() @@ -53,7 +50,7 @@ func Repo(ctx *context.APIContext) { return } - repo.AttributedTo = ap.IRI(strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name) + repo.AttributedTo = ap.IRI(setting.AppURL + "api/v1/activitypub/user/" + ctx.ContextUser.Name) repo.Summary = ap.NaturalLanguageValuesNew() err = repo.Summary.Set("en", ap.Content(ctx.Repo.Repository.Description)) @@ -70,7 +67,7 @@ func Repo(ctx *context.APIContext) { response(ctx, repo) } -// RepoInbox function +// RepoInbox function handles the incoming data for a repo inbox func RepoInbox(ctx *context.APIContext) { // swagger:operation POST /activitypub/repo/{username}/{reponame}/inbox activitypub activitypubRepoInbox // --- @@ -95,29 +92,63 @@ func RepoInbox(ctx *context.APIContext) { body, err := io.ReadAll(ctx.Req.Body) if err != nil { ctx.ServerError("Error reading request body", err) + return } - var activity ap.Activity - activity.UnmarshalJSON(body) // This function doesn't support ForgeFed types!!! - log.Warn("Debug", activity) - switch activity.Type { - case ap.NoteType: - // activitypub.Comment(ctx, activity) + + var activity map[string]interface{} + err = json.Unmarshal(body, activity) + if err != nil { + ctx.ServerError("Unmarshal", err) + return + } + + switch activity["type"].(ap.ActivityVocabularyType) { case ap.CreateType: - // if activity.Object.GetType() == forgefed.RepositoryType { - // Fork created by remote instance - activitypub.ForkFromCreate(ctx, activity) - //} - case ap.MoveType: - // This should actually be forgefed.TicketType but that the UnmarshalJSON function above doesn't support ForgeFed! - activitypub.PullRequest(ctx, activity) + // Create activity, extract the object + object, ok := activity["object"].(map[string]interface{}) + if ok { + ctx.ServerError("Activity does not contain object", err) + return + } + objectBinary, err := json.Marshal(object) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + switch object["type"].(ap.ActivityVocabularyType) { + case forgefed.RepositoryType: + // Fork created by remote instance + var repository forgefed.Repository + repository.UnmarshalJSON(objectBinary) + activitypub.ForkFromCreate(ctx, repository) + case forgefed.TicketType: + // New issue or pull request + var ticket forgefed.Ticket + ticket.UnmarshalJSON(objectBinary) + if ticket.Origin != nil { + // New pull request + activitypub.PullRequest(ctx, ticket) + } else { + // New issue + activitypub.Issue(ctx, ticket) + } + case ap.NoteType: + // New comment + var note ap.Note + note.UnmarshalJSON(objectBinary) + activitypub.Comment(ctx, note) + } default: log.Warn("ActivityStreams type not supported", activity) + ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") + return } ctx.Status(http.StatusNoContent) } -// RepoOutbox function +// RepoOutbox function returns the repo's Outbox OrderedCollection func RepoOutbox(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/outbox activitypub activitypubPersonOutbox // --- @@ -139,34 +170,11 @@ func RepoOutbox(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name - - 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.ServerError("Couldn't fetch outbox", err) - } - - outbox := ap.OrderedCollectionNew(ap.IRI(link + "/outbox")) - 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)) - - response(ctx, outbox) + // TODO + ctx.Status(http.StatusNotImplemented) } -// RepoFollowers function +// RepoFollowers function returns the repo's Followers OrderedCollection func RepoFollowers(ctx *context.APIContext) { // swagger:operation GET /activitypub/repo/{username}/{reponame}/followers activitypub activitypubRepoFollowers // --- @@ -188,21 +196,6 @@ func RepoFollowers(ctx *context.APIContext) { // "200": // "$ref": "#/responses/ActivityPub" - link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/repo/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name - - users, _, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, utils.GetListOptions(ctx)) - if err != nil { - ctx.ServerError("GetUserFollowers", err) - return - } - - followers := ap.OrderedCollectionNew(ap.IRI(link + "/followers")) - followers.TotalItems = uint(len(users)) - - for _, user := range users { - person := ap.PersonNew(ap.IRI(user.Website)) - followers.OrderedItems.Append(person) - } - - response(ctx, followers) + // TODO + ctx.Status(http.StatusNotImplemented) } diff --git a/routers/api/v1/activitypub/response.go b/routers/api/v1/activitypub/response.go index fb14653523..d6a6ef68a1 100644 --- a/routers/api/v1/activitypub/response.go +++ b/routers/api/v1/activitypub/response.go @@ -7,6 +7,7 @@ package activitypub import ( "net/http" + "code.gitea.io/gitea/models/forgefed" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" @@ -15,12 +16,18 @@ import ( "github.com/go-ap/jsonld" ) +// Respond with an ActivityStreams object func response(ctx *context.APIContext, v interface{}) { - binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(v) + 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 {