diff --git a/.deadcode-out b/.deadcode-out index a6714d76a9..3b3b44af7c 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -137,9 +137,6 @@ package "code.gitea.io/gitea/models/user" func GetUserEmailsByNames func GetUserNamesByIDs -package "code.gitea.io/gitea/modules/activitypub" - func (*Client).Post - package "code.gitea.io/gitea/modules/assetfs" func Bindata @@ -168,7 +165,6 @@ package "code.gitea.io/gitea/modules/eventsource" func (*Event).String package "code.gitea.io/gitea/modules/forgefed" - func NewForgeLike func GetItemByType func JSONUnmarshalerFn func NotEmpty diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index e624884db3..cb9e05f791 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -1,5 +1,6 @@ // Copyright 2016 The Gogs Authors. All rights reserved. // Copyright 2020 The Gitea Authors. +// Copyright 2024 The Forgejo Authors. // SPDX-License-Identifier: MIT package user @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/repository" ) // getStarredRepos returns the repos that the user with the specified userID has @@ -155,11 +157,12 @@ func Star(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repository.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, true) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return } + ctx.Status(http.StatusNoContent) } @@ -185,7 +188,7 @@ func Unstar(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repository.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, false) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 1d599c5cfb..fa3f0e86de 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -1,5 +1,6 @@ // Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo @@ -332,7 +333,7 @@ func ActionWatch(watch bool) func(ctx *context.Context) { func ActionStar(star bool) func(ctx *context.Context) { return func(ctx *context.Context) { - err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, star) + err := repo_service.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, star) if err != nil { ctx.ServerError(fmt.Sprintf("Action (star, %t)", star), err) return diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go index 1c99f784bc..a7d9b6ef80 100644 --- a/services/federation/federation_service.go +++ b/services/federation/federation_service.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strings" + "time" "code.gitea.io/gitea/models/forgefed" "code.gitea.io/gitea/models/repo" @@ -242,3 +243,41 @@ func StoreFollowingRepoList(ctx context.Context, localRepoID int64, followingRep func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error { return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{}) } + +func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error { + followingRepos, err := repo.FindFollowingReposByRepoID(ctx, repoID) + log.Info("Federated Repos is: %v", followingRepos) + if err != nil { + return err + } + + likeActivityList := make([]fm.ForgeLike, 0) + for _, followingRepo := range followingRepos { + log.Info("Found following repo: %v", followingRepo) + target := followingRepo.URI + likeActivity, err := fm.NewForgeLike(doer.APActorID(), target, time.Now()) + if err != nil { + return err + } + likeActivityList = append(likeActivityList, likeActivity) + } + + apclient, err := activitypub.NewClient(ctx, &doer, doer.APActorID()) + if err != nil { + return err + } + for i, activity := range likeActivityList { + activity.StartTime = activity.StartTime.Add(time.Duration(i) * time.Second) + json, err := activity.MarshalJSON() + if err != nil { + return err + } + + _, err = apclient.Post(json, fmt.Sprintf("%v/inbox/", activity.Object)) + if err != nil { + log.Error("error %v while sending activity: %q", err, activity) + } + } + + return nil +} diff --git a/services/repository/star.go b/services/repository/star.go new file mode 100644 index 0000000000..505da0f099 --- /dev/null +++ b/services/repository/star.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/federation" +) + +func StarRepoAndSendLikeActivities(ctx context.Context, doer user.User, repoID int64, star bool) error { + if err := repo.StarRepo(ctx, doer.ID, repoID, star); err != nil { + return err + } + + if star && setting.Federation.Enabled { + if err := federation.SendLikeActivities(ctx, doer, repoID); err != nil { + return err + } + } + + return nil +} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 440486d931..edc5d29084 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -77,7 +77,7 @@

{{.RepositoryAPActorID}}

- +
diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index 584c1024de..16ed444e47 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -5,8 +5,10 @@ package integration import ( "fmt" + "io" "net/http" "net/http/httptest" + "strings" "testing" "code.gitea.io/gitea/models/db" @@ -16,8 +18,10 @@ import ( unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + fm "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/validation" gitea_context "code.gitea.io/gitea/services/context" repo_service "code.gitea.io/gitea/services/repository" user_service "code.gitea.io/gitea/services/user" @@ -291,6 +295,36 @@ func TestRepoFollowing(t *testing.T) { `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) fmt.Fprint(res, responseBody) }) + repo1InboxReceivedLike := false + federatedRoutes.HandleFunc("/api/v1/activitypub/repository-id/1/inbox/", + func(res http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) + } + buf := new(strings.Builder) + _, err := io.Copy(buf, req.Body) + if err != nil { + t.Errorf("Error reading body: %q", err) + } + like := fm.ForgeLike{} + err = like.UnmarshalJSON([]byte(buf.String())) + if err != nil { + t.Errorf("Error unmarshalling ForgeLike: %q", err) + } + if isValid, err := validation.IsValid(like); !isValid { + t.Errorf("ForgeLike is not valid: %q", err) + } + + activityType := like.Type + object := like.Object.GetLink().String() + isLikeType := activityType == "Like" + isCorrectObject := strings.HasSuffix(object, "/api/v1/activitypub/repository-id/1") + if !isLikeType || !isCorrectObject { + t.Errorf("Activity is not a like for this repo") + } + + repo1InboxReceivedLike = true + }) federatedRoutes.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) @@ -320,4 +354,16 @@ func TestRepoFollowing(t *testing.T) { FederationHostID: federationHost.ID, }) }) + + t.Run("Star a repo having a following repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + repoLink := fmt.Sprintf("/%s", repo.FullName()) + link := fmt.Sprintf("%s/action/star", repoLink) + req := NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": GetCSRF(t, session, repoLink), + }) + assert.False(t, repo1InboxReceivedLike) + session.MakeRequest(t, req, http.StatusOK) + assert.True(t, repo1InboxReceivedLike) + }) }