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 (
 | 
			
		||||
	"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()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]*$`)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue