diff --git a/models/user/user_repository.go b/models/user/user_repository.go
index f4d34318fc..aead02970c 100644
--- a/models/user/user_repository.go
+++ b/models/user/user_repository.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 )
 
@@ -15,12 +16,38 @@ func init() {
 	db.RegisterModel(new(FederatedUser))
 }
 
-func CreateFederationUser(ctx context.Context, user *FederatedUser) error {
+func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
 	if res, err := validation.IsValid(user); !res {
+		return fmt.Errorf("User is not valid: %v", err)
+	}
+	overwrite := CreateUserOverwriteOptions{
+		IsActive:     util.OptionalBoolFalse,
+		IsRestricted: util.OptionalBoolFalse,
+	}
+
+	// Begin transaction
+	ctx, committer, err := db.TxContext((ctx))
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := CreateUser(ctx, user, &overwrite); err != nil {
+		return err
+	}
+
+	federatedUser.UserID = user.ID
+	if res, err := validation.IsValid(federatedUser); !res {
 		return fmt.Errorf("FederatedUser is not valid: %v", err)
 	}
-	_, err := db.GetEngine(ctx).Insert(user)
-	return err
+
+	_, err = db.GetEngine(ctx).Insert(federatedUser)
+	if err != nil {
+		return err
+	}
+
+	// Commit transaction
+	return committer.Commit()
 }
 
 func FindFederatedUser(ctx context.Context, externalID string,
diff --git a/models/user/user_service.go b/models/user/user_service.go
deleted file mode 100644
index 44814ac2ff..0000000000
--- a/models/user/user_service.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package user
-
-import (
-	"context"
-	"fmt"
-	"net/url"
-	"strings"
-
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/forgefed"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/util"
-	"github.com/google/uuid"
-	pwd_gen "github.com/sethvargo/go-password/password"
-)
-
-func CreateFederatedUserFromAP(ctx context.Context, person forgefed.ForgePerson,
-	personID forgefed.PersonID, federationHostID int64) (*User, *FederatedUser, error) {
-
-	localFqdn, err := url.ParseRequestURI(setting.AppURL)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
-	loginName := personID.AsLoginName()
-	name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
-	log.Info("RepositoryInbox: person.Name: %v", person.Name)
-	fullName := person.Name.String()
-	if len(person.Name) == 0 {
-		fullName = name
-	}
-
-	password, err := pwd_gen.Generate(32, 10, 10, false, true)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	user := User{
-		LowerName:                    strings.ToLower(person.PreferredUsername.String()),
-		Name:                         name,
-		FullName:                     fullName,
-		Email:                        email,
-		EmailNotificationsPreference: "disabled",
-		Passwd:                       password,
-		MustChangePassword:           false,
-		LoginName:                    loginName,
-		Type:                         UserTypeRemoteUser,
-		IsAdmin:                      false,
-	}
-
-	overwrite := &CreateUserOverwriteOptions{
-		IsActive:     util.OptionalBoolFalse,
-		IsRestricted: util.OptionalBoolFalse,
-	}
-
-	// Begin transaction
-	ctx, committer, err := db.TxContext((ctx))
-	if err != nil {
-		return nil, nil, err
-	}
-	defer committer.Close()
-
-	if err := CreateUser(ctx, &user, overwrite); err != nil {
-		return nil, nil, err
-	}
-
-	federatedUser, err := NewFederatedUser(user.ID, personID.ID, federationHostID)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	err = CreateFederationUser(ctx, &federatedUser)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	// Commit transaction
-	committer.Commit()
-
-	return &user, &federatedUser, nil
-}
diff --git a/modules/forgefed/federation_service.go b/modules/forgefed/federation_service.go
new file mode 100644
index 0000000000..27408550d9
--- /dev/null
+++ b/modules/forgefed/federation_service.go
@@ -0,0 +1,202 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgefed
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	forgefed_model "code.gitea.io/gitea/models/forgefed"
+	"code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"github.com/google/uuid"
+
+	api "code.gitea.io/gitea/modules/activitypub"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/validation"
+
+	pwd_gen "github.com/sethvargo/go-password/password"
+)
+
+func LikeActivity(ctx *context.APIContext, form any, repositoryId int64) (error, int, string) {
+	activity := form.(*forgefed_model.ForgeLike)
+	if res, err := validation.IsValid(activity); !res {
+		return err, http.StatusNotAcceptable, "RepositoryInbox: Validate activity"
+	}
+	log.Info("RepositoryInbox: activity validated:%v", activity)
+
+	// parse actorID (person)
+	actorURI := activity.Actor.GetID().String()
+	rawActorID, err := forgefed_model.NewActorID(actorURI)
+	if err != nil {
+		return err, http.StatusInternalServerError, "RepositoryInbox: Validating ActorID"
+	}
+	federationHost, err := forgefed_model.FindFederationHostByFqdn(ctx, rawActorID.Host)
+	if err != nil {
+		return err, http.StatusInternalServerError, "RepositoryInbox: Error while loading FederationInfo"
+	}
+	if federationHost == nil {
+		result, err := CreateFederationHostFromAP(ctx, rawActorID)
+		if err != nil {
+			return err, http.StatusNotAcceptable, "RepositoryInbox: Validate actorId"
+		}
+		federationHost = result
+		log.Info("RepositoryInbox: federationInfo validated: %v", federationHost)
+	}
+	if !activity.IsNewer(federationHost.LatestActivity) {
+		return fmt.Errorf("Activity already processed"), http.StatusNotAcceptable, "RepositoryInbox: Validate Activity"
+	}
+
+	actorID, err := forgefed_model.NewPersonID(actorURI, string(federationHost.NodeInfo.Source))
+	if err != nil {
+		return err, http.StatusNotAcceptable, "RepositoryInbox: Validate actorId"
+	}
+	log.Info("RepositoryInbox: actorId validated: %v", actorID)
+	// parse objectID (repository)
+	objectID, err := forgefed_model.NewRepositoryID(activity.Object.GetID().String(), string(forgefed_model.ForgejoSourceType))
+	if err != nil {
+		return err, http.StatusNotAcceptable, "RepositoryInbox: Validate objectId"
+	}
+	if objectID.ID != fmt.Sprint(repositoryId) {
+		return err, http.StatusNotAcceptable, "RepositoryInbox: Validate objectId"
+	}
+	log.Info("RepositoryInbox: objectId validated: %v", objectID)
+
+	actorAsLoginID := actorID.AsLoginName() // used as LoginName in newly created user
+	log.Info("RepositoryInbox: remoteStargazer: %v", actorAsLoginID)
+
+	// Check if user already exists
+	user, _, err := user_model.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
+	if err != nil {
+		return err, http.StatusInternalServerError, "RepositoryInbox: Searching for user failed"
+	}
+	if user != nil {
+		log.Info("RepositoryInbox: found user: %v", user)
+	} else {
+		user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID)
+		if err != nil {
+			return err, http.StatusInternalServerError,
+				"RepositoryInbox: Creating federated user failed"
+		}
+		log.Info("RepositoryInbox: created user from ap: %v", user)
+	}
+
+	// execute the activity if the repo was not stared already
+	alreadyStared := repo.IsStaring(ctx, user.ID, repositoryId)
+	if !alreadyStared {
+		err = repo.StarRepo(ctx, user.ID, repositoryId, true)
+		if err != nil {
+			return err, http.StatusNotAcceptable, "RepositoryInbox: Star operation"
+		}
+	}
+	federationHost.LatestActivity = activity.StartTime
+	err = forgefed_model.UpdateFederationHost(ctx, *federationHost)
+	if err != nil {
+		return err, http.StatusNotAcceptable, "RepositoryInbox: error updateing federateionInfo"
+	}
+
+	return nil, 0, ""
+}
+
+func CreateFederationHostFromAP(ctx *context.APIContext, actorID forgefed_model.ActorID) (*forgefed_model.FederationHost, error) {
+	actionsUser := user_model.NewActionsUser()
+	client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.")
+	if err != nil {
+		return nil, err
+	}
+	body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
+	if err != nil {
+		return nil, err
+	}
+	nodeInfoWellKnown, err := forgefed_model.NewNodeInfoWellKnown(body)
+	if err != nil {
+		return nil, err
+	}
+	body, err = client.GetBody(nodeInfoWellKnown.Href)
+	if err != nil {
+		return nil, err
+	}
+	nodeInfo, err := forgefed_model.NewNodeInfo(body)
+	if err != nil {
+		return nil, err
+	}
+	result, err := forgefed_model.NewFederationHost(nodeInfo, actorID.Host)
+	if err != nil {
+		return nil, err
+	}
+	err = forgefed_model.CreateFederationHost(ctx, result)
+	if err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+func CreateUserFromAP(ctx *context.APIContext, personID forgefed_model.PersonID, federationHostID int64) (*user_model.User, *user_model.FederatedUser, error) {
+	// ToDo: Do we get a publicKeyId from server, repo or owner or repo?
+	actionsUser := user_model.NewActionsUser()
+	client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := client.GetBody(personID.AsURI())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	person := forgefed_model.ForgePerson{}
+	err = person.UnmarshalJSON(body)
+	if err != nil {
+		return nil, nil, err
+	}
+	if res, err := validation.IsValid(person); !res {
+		return nil, nil, err
+	}
+	log.Info("RepositoryInbox: validated person: %q", person)
+
+	localFqdn, err := url.ParseRequestURI(setting.AppURL)
+	if err != nil {
+		return nil, nil, err
+	}
+	email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
+	loginName := personID.AsLoginName()
+	name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
+	log.Info("RepositoryInbox: person.Name: %v", person.Name)
+	fullName := person.Name.String()
+	if len(person.Name) == 0 {
+		fullName = name
+	}
+	password, err := pwd_gen.Generate(32, 10, 10, false, true)
+	if err != nil {
+		return nil, nil, err
+	}
+	user := user_model.User{
+		LowerName:                    strings.ToLower(person.PreferredUsername.String()),
+		Name:                         name,
+		FullName:                     fullName,
+		Email:                        email,
+		EmailNotificationsPreference: "disabled",
+		Passwd:                       password,
+		MustChangePassword:           false,
+		LoginName:                    loginName,
+		Type:                         user_model.UserTypeRemoteUser,
+		IsAdmin:                      false,
+	}
+
+	federatedUser := user_model.FederatedUser{
+		ExternalID:       personID.ID,
+		FederationHostID: federationHostID,
+	}
+
+	err = user_model.CreateFederatedUser(ctx, &user, &federatedUser)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return &user, &federatedUser, nil
+}
diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go
index ddfcab7151..36afe8e0ed 100644
--- a/routers/api/v1/activitypub/repository.go
+++ b/routers/api/v1/activitypub/repository.go
@@ -8,14 +8,12 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models/forgefed"
-	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
-	api "code.gitea.io/gitea/modules/activitypub"
+	forgefed_model "code.gitea.io/gitea/models/forgefed"
+
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/forgefed"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 
 	ap "github.com/go-ap/activitypub"
@@ -39,7 +37,7 @@ func Repository(ctx *context.APIContext) {
 	//     "$ref": "#/responses/ActivityPub"
 
 	link := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID)
-	repo := forgefed.RepositoryNew(ap.IRI(link))
+	repo := forgefed_model.RepositoryNew(ap.IRI(link))
 
 	repo.Name = ap.NaturalLanguageValuesNew()
 	err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name))
@@ -75,160 +73,11 @@ func RepositoryInbox(ctx *context.APIContext) {
 	repository := ctx.Repo.Repository
 	log.Info("RepositoryInbox: repo: %v", repository)
 
-	activity := web.GetForm(ctx).(*forgefed.ForgeLike)
-	if res, err := validation.IsValid(activity); !res {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate activity", err)
-		return
-	}
-	log.Info("RepositoryInbox: activity validated:%v", activity)
-
-	// parse actorID (person)
-	actorURI := activity.Actor.GetID().String()
-	rawActorID, err := forgefed.NewActorID(actorURI)
+	form := web.GetForm(ctx)
+	err, httpStatus, title := forgefed.LikeActivity(ctx, form, repository.ID)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError,
-			"RepositoryInbox: Validating ActorID", err)
-		return
-	}
-	federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError,
-			"RepositoryInbox: Error while loading FederationInfo", err)
-		return
-	}
-	if federationHost == nil {
-		result, err := createFederationHost(ctx, rawActorID)
-		if err != nil {
-			ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate actorId", err)
-			return
-		}
-		federationHost = &result
-		log.Info("RepositoryInbox: federationInfo validated: %v", federationHost)
-	}
-	if !activity.IsNewer(federationHost.LatestActivity) {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate Activity",
-			fmt.Errorf("Activity already processed"))
-		return
-	}
-
-	actorID, err := forgefed.NewPersonID(actorURI, string(federationHost.NodeInfo.Source))
-	if err != nil {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate actorId", err)
-		return
-	}
-	log.Info("RepositoryInbox: actorId validated: %v", actorID)
-	// parse objectID (repository)
-	objectID, err := forgefed.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
-	if err != nil {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate objectId", err)
-		return
-	}
-	if objectID.ID != fmt.Sprint(repository.ID) {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate objectId", err)
-		return
-	}
-	log.Info("RepositoryInbox: objectId validated: %v", objectID)
-
-	actorAsLoginID := actorID.AsLoginName() // used as LoginName in newly created user
-	log.Info("RepositoryInbox: remoteStargazer: %v", actorAsLoginID)
-
-	// Check if user already exists
-	user, _, err := user_model.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "RepositoryInbox: Searching for user failed", err)
-		return
-	}
-	if user != nil {
-		log.Info("RepositoryInbox: found user: %v", user)
-	} else {
-		user, err = createUserFromAP(ctx, actorID, federationHost.ID)
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError,
-				"RepositoryInbox: Creating federated user failed", err)
-			return
-		}
-		log.Info("RepositoryInbox: created user from ap: %v", user)
-	}
-
-	// execute the activity if the repo was not stared already
-	alreadyStared := repo_model.IsStaring(ctx, user.ID, repository.ID)
-	if !alreadyStared {
-		err = repo_model.StarRepo(ctx, user.ID, repository.ID, true)
-		if err != nil {
-			ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Star operation", err)
-			return
-		}
-	}
-	federationHost.LatestActivity = activity.StartTime
-	err = forgefed.UpdateFederationHost(ctx, *federationHost)
-	if err != nil {
-		ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: error updateing federateionInfo", err)
-		return
+		ctx.Error(httpStatus, title, err)
 	}
 
 	ctx.Status(http.StatusNoContent)
 }
-
-func createFederationHost(ctx *context.APIContext, actorID forgefed.ActorID) (forgefed.FederationHost, error) {
-	actionsUser := user_model.NewActionsUser()
-	client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.")
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	body, err = client.GetBody(nodeInfoWellKnown.Href)
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	nodeInfo, err := forgefed.NewNodeInfo(body)
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	err = forgefed.CreateFederationHost(ctx, result)
-	if err != nil {
-		return forgefed.FederationHost{}, err
-	}
-	return result, nil
-}
-
-func createUserFromAP(ctx *context.APIContext, personID forgefed.PersonID, federationHostID int64) (*user_model.User, error) {
-	// ToDo: Do we get a publicKeyId from server, repo or owner or repo?
-	actionsUser := user_model.NewActionsUser()
-	client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.")
-	if err != nil {
-		return nil, err
-	}
-
-	body, err := client.GetBody(personID.AsURI())
-	if err != nil {
-		return nil, err
-	}
-
-	person := forgefed.ForgePerson{}
-	err = person.UnmarshalJSON(body)
-	if err != nil {
-		return nil, err
-	}
-	if res, err := validation.IsValid(person); !res {
-		return nil, err
-	}
-	log.Info("RepositoryInbox: validated person: %q", person)
-
-	user, _, err := user_model.CreateFederatedUserFromAP(ctx, person, personID, federationHostID)
-	if err != nil {
-		return nil, err
-	}
-
-	return user, nil
-}