mirror of
1
Fork 0

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 ( import (
"time" "time"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
ap "github.com/go-ap/activitypub" ap "github.com/go-ap/activitypub"
@ -18,6 +19,21 @@ type ForgeLike struct {
ap.Activity 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) { func (like ForgeLike) MarshalJSON() ([]byte, error) {
return like.Activity.MarshalJSON() 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) 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. // GetCommitsCountCacheKey returns cache key used for commits count caching.
func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string { func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string {
var prefix string var prefix string

View File

@ -301,6 +301,11 @@ func (u *User) HTMLURL() string {
return setting.AppURL + url.PathEscape(u.Name) 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. // OrganisationLink returns the organization sub page link.
func (u *User) OrganisationLink() string { func (u *User) OrganisationLink() string {
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)

View File

@ -22,6 +22,14 @@ import (
"github.com/google/uuid" "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) { func LikeActivity(ctx *context.APIContext, form any, repositoryID int64) (int, string, error) {
activity := form.(*forgefed.ForgeLike) activity := form.(*forgefed.ForgeLike)
if res, err := validation.IsValid(activity); !res { 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) federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
if err != nil { if err != nil {
return http.StatusInternalServerError, "Could not loading FederationHost", err return http.StatusInternalServerError, "Could not load FederationHost", err
} }
if federationHost == nil { if federationHost == nil {
result, err := CreateFederationHostFromAP(ctx, rawActorID) result, err := CreateFederationHostFromAP(ctx, rawActorID)

View File

@ -157,6 +157,10 @@ func IsValidFederatedRepoURLList(urls string) bool {
return true return true
} }
func IsOfValidLength(str string) bool {
return len(str) <= 2048
}
var ( var (
validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
validUsernamePatternWithoutDots = 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 { if isValid {
return []string{} 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 { func ValidateMaxLen(value string, maxLen int, fieldName string) []string {

View File

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

View File

@ -7,12 +7,16 @@ package user
import ( import (
std_context "context" std_context "context"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/forgefed"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" 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/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
@ -160,6 +164,32 @@ func Star(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "StarRepo", err) ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return 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) ctx.Status(http.StatusNoContent)
} }

View File

@ -192,11 +192,17 @@ func SettingsPost(ctx *context.Context) {
return return
} }
// ToDo: Use Federated Repo Struct & Update Federated Repo Table
switch { switch {
// Allow clearing the field // Allow clearing the field
case form.FederationRepos == "": case form.FederationRepos == "":
repo.FederationRepos = "" repo.FederationRepos = ""
// Validate // 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): case validation.IsValidFederatedRepoURL(form.FederationRepos):
repo.FederationRepos = form.FederationRepos repo.FederationRepos = form.FederationRepos
default: default: