Merge branch 'forgejo-federated-star' of codeberg.org:meissa/forgejo into forgejo-federated-star

This commit is contained in:
Michael Jerger 2024-03-22 07:32:30 +01:00
commit 7316108d56
9 changed files with 77 additions and 3 deletions

View file

@ -6,6 +6,7 @@ package forgefed
import (
"time"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub"
@ -18,6 +19,21 @@ type ForgeLike struct {
ap.Activity
}
func NewForgeLike(ctx *context.APIContext) (ForgeLike, error) {
result := ForgeLike{}
actorIRI := ctx.Repo.Owner.APAPIURL()
objectIRI := ctx.Repo.Repository.APAPIURL()
result.Type = ap.LikeType
// ToDo: Would validating the source by Actor.Type field make sense?
result.Actor = ap.ActorNew(ap.IRI(actorIRI), "ForgejoUser") // Thats us, a User
result.Object = ap.ObjectNew(ap.ActivityVocabularyType(objectIRI)) // Thats them, a Repository
result.StartTime = time.Now()
if valid, err := validation.IsValid(result); !valid {
return ForgeLike{}, err
}
return result, nil
}
func (like ForgeLike) MarshalJSON() ([]byte, error) {
return like.Activity.MarshalJSON()
}

View file

@ -346,6 +346,11 @@ func (repo *Repository) APIURL() string {
return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
}
// APAPIURL returns the activitypub repository API URL
func (repo *Repository) APAPIURL() string {
return setting.AppURL + "api/v1/activitypub/repository-id/" + url.PathEscape(string(repo.ID))
}
// GetCommitsCountCacheKey returns cache key used for commits count caching.
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
var prefix string

View file

@ -301,6 +301,11 @@ func (u *User) HTMLURL() string {
return setting.AppURL + url.PathEscape(u.Name)
}
// APAPIURL returns the IRI to the api endpoint of the user
func (u *User) APAPIURL() string {
return setting.AppURL + url.PathEscape("api/v1/activitypub/user-id/") + url.PathEscape(string(u.ID))
}
// OrganisationLink returns the organization sub page link.
func (u *User) OrganisationLink() string {
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)

View file

@ -22,6 +22,14 @@ import (
"github.com/google/uuid"
)
// ToDo: May need to change the name to reflect workings of function better
// LikeActivity receives a ForgeLike activity and does the following:
// Validation of the activity
// Creation of a (remote) federationHost if not existing
// Creation of a forgefed Person if not existing
// Validation of incoming RepositoryID against Local RepositoryID
// Star the repo if it wasn't already stared
// Do some mitigation against out of order attacks
func LikeActivity(ctx *context.APIContext, form any, repositoryID int64) (int, string, error) {
activity := form.(*forgefed.ForgeLike)
if res, err := validation.IsValid(activity); !res {
@ -37,7 +45,7 @@ func LikeActivity(ctx *context.APIContext, form any, repositoryID int64) (int, s
}
federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
if err != nil {
return http.StatusInternalServerError, "Could not loading FederationHost", err
return http.StatusInternalServerError, "Could not load FederationHost", err
}
if federationHost == nil {
result, err := CreateFederationHostFromAP(ctx, rawActorID)

View file

@ -157,6 +157,10 @@ func IsValidFederatedRepoURLList(urls string) bool {
return true
}
func IsOfValidLength(str string) bool {
return len(str) <= 2048
}
var (
validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`)

View file

@ -46,7 +46,7 @@ func ValidateNotEmpty(value any, fieldName string) []string {
if isValid {
return []string{}
}
return []string{fmt.Sprintf("Field %v may not be empty", fieldName)}
return []string{fmt.Sprintf("Field %v should not be empty", fieldName)}
}
func ValidateMaxLen(value string, maxLen int, fieldName string) []string {

View file

@ -898,7 +898,7 @@ func Routes() *web.Route {
}, context_service.UserIDAssignmentAPI())
m.Group("/repository-id/{repository-id}", func() {
m.Get("", activitypub.Repository)
m.Post("/inbox", // ToDo: Post or Put?
m.Post("/inbox",
// TODO: bind ativities here
bind(forgefed.ForgeLike{}),
// TODO: activitypub.ReqHTTPSignature(),

View file

@ -7,12 +7,16 @@ package user
import (
std_context "context"
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/forgefed"
access_model "code.gitea.io/gitea/models/perm/access"
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/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert"
@ -160,6 +164,32 @@ func Star(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
}
if setting.Federation.Enabled {
likeActivity, err := forgefed.NewForgeLike(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
}
json, err := likeActivity.MarshalJSON()
if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
}
apclient, err := activitypub.NewClient(ctx, ctx.Doer, ctx.Doer.APAPIURL())
if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
}
// ToDo: Change this to the standalone table of FederatedRepos
for _, target := range strings.Split(ctx.Repo.Repository.FederationRepos, ";") {
apclient.Post([]byte(json), target)
}
// Send to list of federated repos
}
ctx.Status(http.StatusNoContent)
}

View file

@ -192,11 +192,17 @@ func SettingsPost(ctx *context.Context) {
return
}
// ToDo: Use Federated Repo Struct & Update Federated Repo Table
switch {
// Allow clearing the field
case form.FederationRepos == "":
repo.FederationRepos = ""
// Validate
case !validation.IsOfValidLength(form.FederationRepos): // ToDo: Use for public testing only. In production we might need longer strings.
ctx.Data["ERR_FederationRepos"] = true
ctx.Flash.Error("The given string was larger than 2048 bytes")
ctx.Redirect(repo.Link() + "/settings")
return
case validation.IsValidFederatedRepoURL(form.FederationRepos):
repo.FederationRepos = form.FederationRepos
default: