mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-10 20:01:24 +01:00
Merge branch 'forgejo-federated-star' of codeberg.org:meissa/forgejo into forgejo-federated-star
This commit is contained in:
commit
7316108d56
9 changed files with 77 additions and 3 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]*$`)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue