follow.go (11893B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package account 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 25 "github.com/superseriousbusiness/gotosocial/internal/ap" 26 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 27 "github.com/superseriousbusiness/gotosocial/internal/db" 28 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 29 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 30 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 31 "github.com/superseriousbusiness/gotosocial/internal/id" 32 "github.com/superseriousbusiness/gotosocial/internal/messages" 33 "github.com/superseriousbusiness/gotosocial/internal/uris" 34 ) 35 36 // FollowCreate handles a follow request to an account, either remote or local. 37 func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { 38 targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, form.ID) 39 if errWithCode != nil { 40 return nil, errWithCode 41 } 42 43 // Check if a follow exists already. 44 if follow, err := p.state.DB.GetFollow( 45 gtscontext.SetBarebones(ctx), 46 requestingAccount.ID, 47 targetAccount.ID, 48 ); err != nil && !errors.Is(err, db.ErrNoEntries) { 49 err = fmt.Errorf("FollowCreate: db error checking existing follow: %w", err) 50 return nil, gtserror.NewErrorInternalError(err) 51 } else if follow != nil { 52 // Already follows, update if necessary + return relationship. 53 return p.updateFollow( 54 ctx, 55 requestingAccount, 56 form, 57 follow.ShowReblogs, 58 follow.Notify, 59 func(columns ...string) error { return p.state.DB.UpdateFollow(ctx, follow, columns...) }, 60 ) 61 } 62 63 // Check if a follow request exists already. 64 if followRequest, err := p.state.DB.GetFollowRequest( 65 gtscontext.SetBarebones(ctx), 66 requestingAccount.ID, 67 targetAccount.ID, 68 ); err != nil && !errors.Is(err, db.ErrNoEntries) { 69 err = fmt.Errorf("FollowCreate: db error checking existing follow request: %w", err) 70 return nil, gtserror.NewErrorInternalError(err) 71 } else if followRequest != nil { 72 // Already requested, update if necessary + return relationship. 73 return p.updateFollow( 74 ctx, 75 requestingAccount, 76 form, 77 followRequest.ShowReblogs, 78 followRequest.Notify, 79 func(columns ...string) error { return p.state.DB.UpdateFollowRequest(ctx, followRequest, columns...) }, 80 ) 81 } 82 83 // Neither follows nor follow requests, so 84 // create and store a new follow request. 85 followID, err := id.NewRandomULID() 86 if err != nil { 87 return nil, gtserror.NewErrorInternalError(err) 88 } 89 followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID) 90 91 fr := >smodel.FollowRequest{ 92 ID: followID, 93 URI: followURI, 94 AccountID: requestingAccount.ID, 95 Account: requestingAccount, 96 TargetAccountID: form.ID, 97 TargetAccount: targetAccount, 98 ShowReblogs: form.Reblogs, 99 Notify: form.Notify, 100 } 101 102 if err := p.state.DB.PutFollowRequest(ctx, fr); err != nil { 103 err = fmt.Errorf("FollowCreate: error creating follow request in db: %s", err) 104 return nil, gtserror.NewErrorInternalError(err) 105 } 106 107 if targetAccount.IsLocal() && !*targetAccount.Locked { 108 // If the target account is local and not locked, 109 // we can already accept the follow request and 110 // skip any further processing. 111 // 112 // Because we know the requestingAccount is also 113 // local, we don't need to federate the accept out. 114 if _, err := p.state.DB.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil { 115 err = fmt.Errorf("FollowCreate: error accepting follow request for local unlocked account: %w", err) 116 return nil, gtserror.NewErrorInternalError(err) 117 } 118 } else if targetAccount.IsRemote() { 119 // Otherwise we leave the follow request as it is, 120 // and we handle the rest of the process async. 121 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 122 APObjectType: ap.ActivityFollow, 123 APActivityType: ap.ActivityCreate, 124 GTSModel: fr, 125 OriginAccount: requestingAccount, 126 TargetAccount: targetAccount, 127 }) 128 } 129 130 return p.RelationshipGet(ctx, requestingAccount, form.ID) 131 } 132 133 // FollowRemove handles the removal of a follow/follow request to an account, either remote or local. 134 func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { 135 targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, targetAccountID) 136 if errWithCode != nil { 137 return nil, errWithCode 138 } 139 140 // Unfollow and deal with side effects. 141 msgs, err := p.unfollow(ctx, requestingAccount, targetAccount) 142 if err != nil { 143 return nil, gtserror.NewErrorNotFound(fmt.Errorf("FollowRemove: account %s not found in the db: %s", targetAccountID, err)) 144 } 145 146 // Batch queue accreted client api messages. 147 p.state.Workers.EnqueueClientAPI(ctx, msgs...) 148 149 return p.RelationshipGet(ctx, requestingAccount, targetAccountID) 150 } 151 152 /* 153 Utility functions. 154 */ 155 156 // updateFollow is a utility function for updating an existing 157 // follow or followRequest with the parameters provided in the 158 // given form. If nothing changes, this function is a no-op and 159 // will just return the existing relationship between follow 160 // origin and follow target account. 161 func (p *Processor) updateFollow( 162 ctx context.Context, 163 requestingAccount *gtsmodel.Account, 164 form *apimodel.AccountFollowRequest, 165 currentShowReblogs *bool, 166 currentNotify *bool, 167 update func(...string) error, 168 ) (*apimodel.Relationship, gtserror.WithCode) { 169 170 if form.Reblogs == nil && form.Notify == nil { 171 // There's nothing to update. 172 return p.RelationshipGet(ctx, requestingAccount, form.ID) 173 } 174 175 // Including "updated_at", max 3 columns may change. 176 columns := make([]string, 0, 3) 177 178 // Check what we need to update (if anything). 179 if newReblogs := form.Reblogs; newReblogs != nil && *newReblogs != *currentShowReblogs { 180 *currentShowReblogs = *newReblogs 181 columns = append(columns, "show_reblogs") 182 } 183 184 if newNotify := form.Notify; newNotify != nil && *newNotify != *currentNotify { 185 *currentNotify = *newNotify 186 columns = append(columns, "notify") 187 } 188 189 if len(columns) == 0 { 190 // Nothing actually changed. 191 return p.RelationshipGet(ctx, requestingAccount, form.ID) 192 } 193 194 if err := update(columns...); err != nil { 195 err = fmt.Errorf("updateFollow: error updating existing follow (request): %w", err) 196 return nil, gtserror.NewErrorInternalError(err) 197 } 198 199 return p.RelationshipGet(ctx, requestingAccount, form.ID) 200 } 201 202 // getFollowTarget is a convenience function which: 203 // - Checks if account is trying to follow/unfollow itself. 204 // - Returns not found if there's a block in place between accounts. 205 // - Returns target account according to its id. 206 func (p *Processor) getFollowTarget(ctx context.Context, requestingAccountID string, targetAccountID string) (*gtsmodel.Account, gtserror.WithCode) { 207 // Account can't follow or unfollow itself. 208 if requestingAccountID == targetAccountID { 209 err := errors.New("account can't follow or unfollow itself") 210 return nil, gtserror.NewErrorNotAcceptable(err) 211 } 212 213 // Do nothing if a block exists in either direction between accounts. 214 if blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccountID, targetAccountID); err != nil { 215 err = fmt.Errorf("db error checking block between accounts: %w", err) 216 return nil, gtserror.NewErrorInternalError(err) 217 } else if blocked { 218 err = errors.New("block exists between accounts") 219 return nil, gtserror.NewErrorNotFound(err) 220 } 221 222 // Ensure target account retrievable. 223 targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) 224 if err != nil { 225 if !errors.Is(err, db.ErrNoEntries) { 226 // Real db error. 227 err = fmt.Errorf("db error looking for target account %s: %w", targetAccountID, err) 228 return nil, gtserror.NewErrorInternalError(err) 229 } 230 // Account not found. 231 err = fmt.Errorf("target account %s not found in the db", targetAccountID) 232 return nil, gtserror.NewErrorNotFound(err, err.Error()) 233 } 234 235 return targetAccount, nil 236 } 237 238 // unfollow is a convenience function for having requesting account 239 // unfollow (and un follow request) target account, if follows and/or 240 // follow requests exist. 241 // 242 // If a follow and/or follow request was removed this way, one or two 243 // messages will be returned which should then be processed by a client 244 // api worker. 245 func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]messages.FromClientAPI, error) { 246 var msgs []messages.FromClientAPI 247 248 // Get follow from requesting account to target account. 249 follow, err := p.state.DB.GetFollow(ctx, requestingAccount.ID, targetAccount.ID) 250 if err != nil && !errors.Is(err, db.ErrNoEntries) { 251 err = fmt.Errorf("unfollow: error getting follow from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) 252 return nil, err 253 } 254 255 if follow != nil { 256 // Delete known follow from database with ID. 257 err = p.state.DB.DeleteFollowByID(ctx, follow.ID) 258 if err != nil { 259 if !errors.Is(err, db.ErrNoEntries) { 260 err = fmt.Errorf("unfollow: error deleting request from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) 261 return nil, err 262 } 263 264 // If err == db.ErrNoEntries here then it 265 // indicates a race condition with another 266 // unfollow for the same requester->target. 267 return msgs, nil 268 } 269 270 // Follow status changed, process side effects. 271 msgs = append(msgs, messages.FromClientAPI{ 272 APObjectType: ap.ActivityFollow, 273 APActivityType: ap.ActivityUndo, 274 GTSModel: >smodel.Follow{ 275 AccountID: requestingAccount.ID, 276 TargetAccountID: targetAccount.ID, 277 URI: follow.URI, 278 }, 279 OriginAccount: requestingAccount, 280 TargetAccount: targetAccount, 281 }) 282 } 283 284 // Get follow request from requesting account to target account. 285 followReq, err := p.state.DB.GetFollowRequest(ctx, requestingAccount.ID, targetAccount.ID) 286 if err != nil && !errors.Is(err, db.ErrNoEntries) { 287 err = fmt.Errorf("unfollow: error getting follow request from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) 288 return nil, err 289 } 290 291 if followReq != nil { 292 // Delete known follow request from database with ID. 293 err = p.state.DB.DeleteFollowRequestByID(ctx, followReq.ID) 294 if err != nil { 295 if !errors.Is(err, db.ErrNoEntries) { 296 err = fmt.Errorf("unfollow: error deleting follow request from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) 297 return nil, err 298 } 299 300 // If err == db.ErrNoEntries here then it 301 // indicates a race condition with another 302 // unfollow for the same requester->target. 303 return msgs, nil 304 } 305 306 // Follow status changed, process side effects. 307 msgs = append(msgs, messages.FromClientAPI{ 308 APObjectType: ap.ActivityFollow, 309 APActivityType: ap.ActivityUndo, 310 GTSModel: >smodel.Follow{ 311 AccountID: requestingAccount.ID, 312 TargetAccountID: targetAccount.ID, 313 URI: followReq.URI, 314 }, 315 OriginAccount: requestingAccount, 316 TargetAccount: targetAccount, 317 }) 318 } 319 320 return msgs, nil 321 }