loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'Creation of federated user' (#3792) from meissa/forgejo:forgejo-federated-pr3 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3792
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+386 -24
+2 -10
.deadcode-out
··· 131 131 func (ErrUserInactive).Unwrap 132 132 func IsErrExternalLoginUserAlreadyExist 133 133 func IsErrExternalLoginUserNotExist 134 + func NewFederatedUser 134 135 func IsErrUserSettingIsNotExist 135 136 func GetUserAllSettings 136 137 func DeleteUserSetting 137 138 func GetUserEmailsByNames 138 139 func GetUserNamesByIDs 140 + func DeleteFederatedUser 139 141 140 142 package "code.gitea.io/gitea/modules/activitypub" 141 143 func (*Client).Post ··· 169 171 170 172 package "code.gitea.io/gitea/modules/forgefed" 171 173 func NewForgeLike 172 - func NewPersonID 173 - func (PersonID).AsWebfinger 174 - func (PersonID).AsLoginName 175 - func (PersonID).HostSuffix 176 - func (PersonID).Validate 177 - func NewRepositoryID 178 - func (RepositoryID).Validate 179 - func (ForgePerson).MarshalJSON 180 - func (*ForgePerson).UnmarshalJSON 181 - func (ForgePerson).Validate 182 174 func GetItemByType 183 175 func JSONUnmarshalerFn 184 176 func NotEmpty
+4
models/forgejo_migrations/migrate.go
··· 68 68 NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge), 69 69 // v15 -> v16 70 70 NewMigration("Create the `federation_host` table", CreateFederationHostTable), 71 + // v16 -> v17 72 + NewMigration("Create the `federated_user` table", CreateFederatedUserTable), 73 + // v17 -> v18 74 + NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser), 71 75 } 72 76 73 77 // GetCurrentDBVersion returns the current Forgejo database version.
+17
models/forgejo_migrations/v16.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package forgejo_migrations //nolint:revive 5 + 6 + import "xorm.io/xorm" 7 + 8 + type FederatedUser struct { 9 + ID int64 `xorm:"pk autoincr"` 10 + UserID int64 `xorm:"NOT NULL"` 11 + ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` 12 + FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` 13 + } 14 + 15 + func CreateFederatedUserTable(x *xorm.Engine) error { 16 + return x.Sync(new(FederatedUser)) 17 + }
+14
models/forgejo_migrations/v17.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package forgejo_migrations //nolint:revive 5 + 6 + import "xorm.io/xorm" 7 + 8 + func AddNormalizedFederatedURIToUser(x *xorm.Engine) error { 9 + type User struct { 10 + ID int64 `xorm:"pk autoincr"` 11 + NormalizedFederatedURI string 12 + } 13 + return x.Sync(&User{}) 14 + }
+35
models/user/federated_user.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package user 5 + 6 + import ( 7 + "code.gitea.io/gitea/modules/validation" 8 + ) 9 + 10 + type FederatedUser struct { 11 + ID int64 `xorm:"pk autoincr"` 12 + UserID int64 `xorm:"NOT NULL"` 13 + ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` 14 + FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` 15 + } 16 + 17 + func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) { 18 + result := FederatedUser{ 19 + UserID: userID, 20 + ExternalID: externalID, 21 + FederationHostID: federationHostID, 22 + } 23 + if valid, err := validation.IsValid(result); !valid { 24 + return FederatedUser{}, err 25 + } 26 + return result, nil 27 + } 28 + 29 + func (user FederatedUser) Validate() []string { 30 + var result []string 31 + result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...) 32 + result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...) 33 + result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...) 34 + return result 35 + }
+29
models/user/federated_user_test.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package user 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/modules/validation" 10 + ) 11 + 12 + func Test_FederatedUserValidation(t *testing.T) { 13 + sut := FederatedUser{ 14 + UserID: 12, 15 + ExternalID: "12", 16 + FederationHostID: 1, 17 + } 18 + if res, err := validation.IsValid(sut); !res { 19 + t.Errorf("sut should be valid but was %q", err) 20 + } 21 + 22 + sut = FederatedUser{ 23 + ExternalID: "12", 24 + FederationHostID: 1, 25 + } 26 + if res, _ := validation.IsValid(sut); res { 27 + t.Errorf("sut should be invalid") 28 + } 29 + }
+20
models/user/user.go
··· 1 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 + // Copyright 2024 The Forgejo Authors. All rights reserved. 3 4 // SPDX-License-Identifier: MIT 4 5 5 6 package user ··· 130 131 Avatar string `xorm:"VARCHAR(2048) NOT NULL"` 131 132 AvatarEmail string `xorm:"NOT NULL"` 132 133 UseCustomAvatar bool 134 + 135 + // For federation 136 + NormalizedFederatedURI string 133 137 134 138 // Counters 135 139 NumFollowers int ··· 301 305 // HTMLURL returns the user or organization's full link. 302 306 func (u *User) HTMLURL() string { 303 307 return setting.AppURL + url.PathEscape(u.Name) 308 + } 309 + 310 + // APActorID returns the IRI to the api endpoint of the user 311 + func (u *User) APActorID() string { 312 + return fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", setting.AppURL, url.PathEscape(fmt.Sprintf("%v", u.ID))) 304 313 } 305 314 306 315 // OrganisationLink returns the organization sub page link. ··· 832 841 } 833 842 834 843 return nil 844 + } 845 + 846 + func (u User) Validate() []string { 847 + var result []string 848 + if err := ValidateUser(&u); err != nil { 849 + result = append(result, err.Error()) 850 + } 851 + if err := ValidateEmail(u.Email); err != nil { 852 + result = append(result, err.Error()) 853 + } 854 + return result 835 855 } 836 856 837 857 // UpdateUserCols update user according special columns
+83
models/user/user_repository.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package user 5 + 6 + import ( 7 + "context" 8 + "fmt" 9 + 10 + "code.gitea.io/gitea/models/db" 11 + "code.gitea.io/gitea/modules/optional" 12 + "code.gitea.io/gitea/modules/validation" 13 + ) 14 + 15 + func init() { 16 + db.RegisterModel(new(FederatedUser)) 17 + } 18 + 19 + func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error { 20 + if res, err := validation.IsValid(user); !res { 21 + return err 22 + } 23 + overwrite := CreateUserOverwriteOptions{ 24 + IsActive: optional.Some(false), 25 + IsRestricted: optional.Some(false), 26 + } 27 + 28 + // Begin transaction 29 + ctx, committer, err := db.TxContext((ctx)) 30 + if err != nil { 31 + return err 32 + } 33 + defer committer.Close() 34 + 35 + if err := CreateUser(ctx, user, &overwrite); err != nil { 36 + return err 37 + } 38 + 39 + federatedUser.UserID = user.ID 40 + if res, err := validation.IsValid(federatedUser); !res { 41 + return err 42 + } 43 + 44 + _, err = db.GetEngine(ctx).Insert(federatedUser) 45 + if err != nil { 46 + return err 47 + } 48 + 49 + // Commit transaction 50 + return committer.Commit() 51 + } 52 + 53 + func FindFederatedUser(ctx context.Context, externalID string, 54 + federationHostID int64, 55 + ) (*User, *FederatedUser, error) { 56 + federatedUser := new(FederatedUser) 57 + user := new(User) 58 + has, err := db.GetEngine(ctx).Where("external_id=? and federation_host_id=?", externalID, federationHostID).Get(federatedUser) 59 + if err != nil { 60 + return nil, nil, err 61 + } else if !has { 62 + return nil, nil, nil 63 + } 64 + has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) 65 + if err != nil { 66 + return nil, nil, err 67 + } else if !has { 68 + return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID) 69 + } 70 + 71 + if res, err := validation.IsValid(*user); !res { 72 + return nil, nil, err 73 + } 74 + if res, err := validation.IsValid(*federatedUser); !res { 75 + return nil, nil, err 76 + } 77 + return user, federatedUser, nil 78 + } 79 + 80 + func DeleteFederatedUser(ctx context.Context, userID int64) error { 81 + _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) 82 + return err 83 + }
+10
models/user/user_test.go
··· 1 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 3 // SPDX-License-Identifier: MIT 3 4 4 5 package user_test ··· 105 106 assert.True(t, found[user_model.UserTypeIndividual], users) 106 107 assert.True(t, found[user_model.UserTypeRemoteUser], users) 107 108 assert.False(t, found[user_model.UserTypeOrganization], users) 109 + } 110 + 111 + func TestAPActorID(t *testing.T) { 112 + user := user_model.User{ID: 1} 113 + url := user.APActorID() 114 + expected := "https://try.gitea.io/api/v1/activitypub/user-id/1" 115 + if url != expected { 116 + t.Errorf("unexpected APActorID, expected: %q, actual: %q", expected, url) 117 + } 108 118 } 109 119 110 120 func TestSearchUsers(t *testing.T) {
-3
routers/api/v1/activitypub/repository.go
··· 74 74 form := web.GetForm(ctx) 75 75 httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID) 76 76 if err != nil { 77 - log.Error("Status: %v", httpStatus) 78 - log.Error("Title: %v", title) 79 - log.Error("Error: %v", err) 80 77 ctx.Error(httpStatus, title, err) 81 78 } 82 79 ctx.Status(http.StatusNoContent)
+101
services/federation/federation_service.go
··· 7 7 "context" 8 8 "fmt" 9 9 "net/http" 10 + "net/url" 11 + "strings" 10 12 11 13 "code.gitea.io/gitea/models/forgefed" 12 14 "code.gitea.io/gitea/models/user" 13 15 "code.gitea.io/gitea/modules/activitypub" 16 + "code.gitea.io/gitea/modules/auth/password" 14 17 fm "code.gitea.io/gitea/modules/forgefed" 15 18 "code.gitea.io/gitea/modules/log" 19 + "code.gitea.io/gitea/modules/setting" 16 20 "code.gitea.io/gitea/modules/validation" 21 + 22 + "github.com/google/uuid" 17 23 ) 18 24 19 25 // ProcessLikeActivity receives a ForgeLike activity and does the following: ··· 40 46 if !activity.IsNewer(federationHost.LatestActivity) { 41 47 return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed") 42 48 } 49 + actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName)) 50 + if err != nil { 51 + return http.StatusNotAcceptable, "Invalid PersonID", err 52 + } 53 + log.Info("Actor accepted:%v", actorID) 54 + 55 + // parse objectID (repository) 56 + objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType)) 57 + if err != nil { 58 + return http.StatusNotAcceptable, "Invalid objectId", err 59 + } 60 + if objectID.ID != fmt.Sprint(repositoryID) { 61 + return http.StatusNotAcceptable, "Invalid objectId", err 62 + } 63 + log.Info("Object accepted:%v", objectID) 64 + 65 + // Check if user already exists 66 + user, _, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID) 67 + if err != nil { 68 + return http.StatusInternalServerError, "Searching for user failed", err 69 + } 70 + if user != nil { 71 + log.Info("Found local federatedUser: %v", user) 72 + } else { 73 + user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID) 74 + if err != nil { 75 + return http.StatusInternalServerError, "Error creating federatedUser", err 76 + } 77 + log.Info("Created federatedUser from ap: %v", user) 78 + } 79 + log.Info("Got user:%v", user.Name) 43 80 44 81 return 0, "", nil 45 82 } ··· 96 133 } 97 134 return federationHost, nil 98 135 } 136 + 137 + func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) { 138 + // ToDo: Do we get a publicKeyId from server, repo or owner or repo? 139 + actionsUser := user.NewActionsUser() 140 + client, err := activitypub.NewClient(ctx, actionsUser, "no idea where to get key material.") 141 + if err != nil { 142 + return nil, nil, err 143 + } 144 + 145 + body, err := client.GetBody(personID.AsURI()) 146 + if err != nil { 147 + return nil, nil, err 148 + } 149 + 150 + person := fm.ForgePerson{} 151 + err = person.UnmarshalJSON(body) 152 + if err != nil { 153 + return nil, nil, err 154 + } 155 + if res, err := validation.IsValid(person); !res { 156 + return nil, nil, err 157 + } 158 + log.Info("Fetched valid person:%q", person) 159 + 160 + localFqdn, err := url.ParseRequestURI(setting.AppURL) 161 + if err != nil { 162 + return nil, nil, err 163 + } 164 + email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname()) 165 + loginName := personID.AsLoginName() 166 + name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix()) 167 + fullName := person.Name.String() 168 + if len(person.Name) == 0 { 169 + fullName = name 170 + } 171 + password, err := password.Generate(32) 172 + if err != nil { 173 + return nil, nil, err 174 + } 175 + newUser := user.User{ 176 + LowerName: strings.ToLower(name), 177 + Name: name, 178 + FullName: fullName, 179 + Email: email, 180 + EmailNotificationsPreference: "disabled", 181 + Passwd: password, 182 + MustChangePassword: false, 183 + LoginName: loginName, 184 + Type: user.UserTypeRemoteUser, 185 + IsAdmin: false, 186 + NormalizedFederatedURI: personID.AsURI(), 187 + } 188 + federatedUser := user.FederatedUser{ 189 + ExternalID: personID.ID, 190 + FederationHostID: federationHostID, 191 + } 192 + err = user.CreateFederatedUser(ctx, &newUser, &federatedUser) 193 + if err != nil { 194 + return nil, nil, err 195 + } 196 + log.Info("Created federatedUser:%q", federatedUser) 197 + 198 + return &newUser, &federatedUser, nil 199 + }
+71 -11
tests/integration/api_activitypub_repository_test.go
··· 91 91 `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) 92 92 fmt.Fprint(res, responseBody) 93 93 }) 94 - federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/2", 94 + federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/15", 95 95 func(res http.ResponseWriter, req *http.Request) { 96 96 // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 97 97 responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` + 98 - `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2","type":"Person",` + 98 + `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15","type":"Person",` + 99 99 `"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/1bb05d9a5f6675ed0272af9ea193063c"},` + 100 - `"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/inbox",` + 101 - `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/outbox","preferredUsername":"stargoose1",` + 102 - `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2",` + 100 + `"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/inbox",` + 101 + `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/outbox","preferredUsername":"stargoose1",` + 102 + `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15",` + 103 103 `"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` + 104 104 `CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` + 105 105 `T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` + ··· 107 107 `LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`) 108 108 fmt.Fprint(res, responseBody) 109 109 }) 110 + federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/30", 111 + func(res http.ResponseWriter, req *http.Request) { 112 + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/3 113 + responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` + 114 + `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30","type":"Person",` + 115 + `"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/9c03f03d1c1f13f21976a22489326fe1"},` + 116 + `"url":"https://federated-repo.prod.meissa.de/stargoose2","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/inbox",` + 117 + `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/outbox","preferredUsername":"stargoose2",` + 118 + `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30",` + 119 + `"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyv5NytsfqpWXSrwuk8a3\n0W1zE13QJioXb/e3opgN2CfKZkdm3hb+4+mGKoU/rCqegnL9/AO0Aw+R8fCHXx44\n` + 120 + `iNkdVpdY8Dzq+tQ9IetPWbyVIBvSzGgvpqfS05JuVPsy8cBX9wByODjr5kq7k1/v\nY1G7E3uh0a/XJc+mZutwGC3gPgR93NSrqsvTPN4wdhCCu9uj02S8OBoKuSYaPkU+\n` + 121 + `tZ4CEDpnclAOw/eNiH4x2irMvVtruEgtlTA5K2I4YJrmtGLidus47FCyc8/zEKUh\nAeiD8KWDvqsQgOhUwcQgRxAnYVCoMD9cnE+WFFRHTuQecNlmdNFs3Cr0yKcWjDde\n` + 122 + `trvnehW7LfPveGb0tHRHPuVAJpncTOidUR5h/7pqMyvKHzuAHWomm9rEaGUxd/7a\nL1CFjAf39+QIEgu0Anj8mIc7CTiz+DQhDz+0jBOsQ0iDXc5GeBz7X9Xv4Jp966nq\n` + 123 + `MUR0GQGXvfZQN9IqMO+WoUVy10Ddhns1EWGlA0x4fecnAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`) 124 + fmt.Fprint(res, responseBody) 125 + }) 110 126 federatedRoutes.HandleFunc("/", 111 127 func(res http.ResponseWriter, req *http.Request) { 112 128 t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) ··· 129 145 "%s/api/v1/activitypub/repository-id/%v/inbox", 130 146 srv.URL, repositoryID) 131 147 132 - activity := []byte(fmt.Sprintf( 148 + timeNow := time.Now().UTC() 149 + 150 + activity1 := []byte(fmt.Sprintf( 133 151 `{"type":"Like",`+ 134 152 `"startTime":"%s",`+ 135 - `"actor":"%s/api/v1/activitypub/user-id/2",`+ 153 + `"actor":"%s/api/v1/activitypub/user-id/15",`+ 136 154 `"object":"%s/api/v1/activitypub/repository-id/%v"}`, 137 - time.Now().UTC().Format(time.RFC3339), 155 + timeNow.Format(time.RFC3339), 138 156 federatedSrv.URL, srv.URL, repositoryID)) 139 - t.Logf("activity: %s", activity) 140 - resp, err := c.Post(activity, repoInboxURL) 157 + t.Logf("activity: %s", activity1) 158 + resp, err := c.Post(activity1, repoInboxURL) 141 159 142 160 assert.NoError(t, err) 143 161 assert.Equal(t, http.StatusNoContent, resp.StatusCode) 144 162 145 - unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"}) 163 + federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"}) 164 + federatedUser := unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "15", FederationHostID: federationHost.ID}) 165 + unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID}) 166 + 167 + // A like activity by a different user of the same federated host. 168 + activity2 := []byte(fmt.Sprintf( 169 + `{"type":"Like",`+ 170 + `"startTime":"%s",`+ 171 + `"actor":"%s/api/v1/activitypub/user-id/30",`+ 172 + `"object":"%s/api/v1/activitypub/repository-id/%v"}`, 173 + // Make sure this activity happens later then the one before 174 + timeNow.Add(time.Second).Format(time.RFC3339), 175 + federatedSrv.URL, srv.URL, repositoryID)) 176 + t.Logf("activity: %s", activity2) 177 + resp, err = c.Post(activity2, repoInboxURL) 178 + 179 + assert.NoError(t, err) 180 + assert.Equal(t, http.StatusNoContent, resp.StatusCode) 181 + 182 + federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID}) 183 + unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID}) 184 + 185 + // The same user sends another like activity 186 + otherRepositoryID := 3 187 + otherRepoInboxURL := fmt.Sprintf( 188 + "%s/api/v1/activitypub/repository-id/%v/inbox", 189 + srv.URL, otherRepositoryID) 190 + activity3 := []byte(fmt.Sprintf( 191 + `{"type":"Like",`+ 192 + `"startTime":"%s",`+ 193 + `"actor":"%s/api/v1/activitypub/user-id/30",`+ 194 + `"object":"%s/api/v1/activitypub/repository-id/%v"}`, 195 + // Make sure this activity happens later then the ones before 196 + timeNow.Add(time.Second*2).Format(time.RFC3339), 197 + federatedSrv.URL, srv.URL, otherRepositoryID)) 198 + t.Logf("activity: %s", activity3) 199 + resp, err = c.Post(activity3, otherRepoInboxURL) 200 + 201 + assert.NoError(t, err) 202 + assert.Equal(t, http.StatusNoContent, resp.StatusCode) 203 + 204 + federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID}) 205 + unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID}) 146 206 }) 147 207 } 148 208