fave.go (6430B)
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 status 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/gtserror" 29 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 30 "github.com/superseriousbusiness/gotosocial/internal/id" 31 "github.com/superseriousbusiness/gotosocial/internal/log" 32 "github.com/superseriousbusiness/gotosocial/internal/messages" 33 "github.com/superseriousbusiness/gotosocial/internal/uris" 34 ) 35 36 // FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists). 37 func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { 38 targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID) 39 if errWithCode != nil { 40 return nil, errWithCode 41 } 42 43 if existingFave != nil { 44 // Status is already faveed. 45 return p.apiStatus(ctx, targetStatus, requestingAccount) 46 } 47 48 // Create and store a new fave 49 faveID := id.NewULID() 50 gtsFave := >smodel.StatusFave{ 51 ID: faveID, 52 AccountID: requestingAccount.ID, 53 Account: requestingAccount, 54 TargetAccountID: targetStatus.AccountID, 55 TargetAccount: targetStatus.Account, 56 StatusID: targetStatus.ID, 57 Status: targetStatus, 58 URI: uris.GenerateURIForLike(requestingAccount.Username, faveID), 59 } 60 61 if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil { 62 err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err) 63 return nil, gtserror.NewErrorInternalError(err) 64 } 65 66 // Process new status fave side effects. 67 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 68 APObjectType: ap.ActivityLike, 69 APActivityType: ap.ActivityCreate, 70 GTSModel: gtsFave, 71 OriginAccount: requestingAccount, 72 TargetAccount: targetStatus.Account, 73 }) 74 75 return p.apiStatus(ctx, targetStatus, requestingAccount) 76 } 77 78 // FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist). 79 func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { 80 targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID) 81 if errWithCode != nil { 82 return nil, errWithCode 83 } 84 85 if existingFave == nil { 86 // Status isn't faveed. 87 return p.apiStatus(ctx, targetStatus, requestingAccount) 88 } 89 90 // We have a fave to remove. 91 if err := p.state.DB.DeleteStatusFaveByID(ctx, existingFave.ID); err != nil { 92 err = fmt.Errorf("FaveRemove: error removing status fave: %w", err) 93 return nil, gtserror.NewErrorInternalError(err) 94 } 95 96 // Process remove status fave side effects. 97 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 98 APObjectType: ap.ActivityLike, 99 APActivityType: ap.ActivityUndo, 100 GTSModel: existingFave, 101 OriginAccount: requestingAccount, 102 TargetAccount: targetStatus.Account, 103 }) 104 105 return p.apiStatus(ctx, targetStatus, requestingAccount) 106 } 107 108 // FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. 109 func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { 110 targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID) 111 if errWithCode != nil { 112 return nil, errWithCode 113 } 114 115 statusFaves, err := p.state.DB.GetStatusFavesForStatus(ctx, targetStatus.ID) 116 if err != nil { 117 return nil, gtserror.NewErrorNotFound(fmt.Errorf("FavedBy: error seeing who faved status: %s", err)) 118 } 119 120 // For each fave, ensure that we're only showing 121 // the requester accounts that they don't block, 122 // and which don't block them. 123 apiAccounts := make([]*apimodel.Account, 0, len(statusFaves)) 124 for _, fave := range statusFaves { 125 if blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, fave.AccountID); err != nil { 126 err = fmt.Errorf("FavedBy: error checking blocks: %w", err) 127 return nil, gtserror.NewErrorInternalError(err) 128 } else if blocked { 129 continue 130 } 131 132 if fave.Account == nil { 133 // Account isn't set for some reason, just skip. 134 log.WithContext(ctx).WithField("fave", fave).Warn("fave had no associated account") 135 continue 136 } 137 138 apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, fave.Account) 139 if err != nil { 140 err = fmt.Errorf("FavedBy: error converting account %s to frontend representation: %w", fave.AccountID, err) 141 return nil, gtserror.NewErrorInternalError(err) 142 } 143 apiAccounts = append(apiAccounts, apiAccount) 144 } 145 146 return apiAccounts, nil 147 } 148 149 func (p *Processor) getFaveTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) { 150 targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID) 151 if errWithCode != nil { 152 return nil, nil, errWithCode 153 } 154 155 if !*targetStatus.Likeable { 156 err := errors.New("status is not faveable") 157 return nil, nil, gtserror.NewErrorForbidden(err, err.Error()) 158 } 159 160 fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID) 161 if err != nil && !errors.Is(err, db.ErrNoEntries) { 162 err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err) 163 return nil, nil, gtserror.NewErrorInternalError(err) 164 } 165 166 return targetStatus, fave, nil 167 }