boost.go (8865B)
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/messages" 31 ) 32 33 // BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well. 34 func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { 35 targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID) 36 if err != nil { 37 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) 38 } 39 if targetStatus.Account == nil { 40 return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) 41 } 42 43 // if targetStatusID refers to a boost, then we should redirect 44 // the target to being the status that was boosted; if we don't 45 // do this, then we end up in weird situations where people 46 // boost boosts, and it looks absolutely bizarre in the UI 47 if targetStatus.BoostOfID != "" { 48 if targetStatus.BoostOf == nil { 49 b, err := p.state.DB.GetStatusByID(ctx, targetStatus.BoostOfID) 50 if err != nil { 51 return nil, gtserror.NewErrorNotFound(fmt.Errorf("couldn't fetch boosted status %s", targetStatus.BoostOfID)) 52 } 53 targetStatus.BoostOf = b 54 } 55 targetStatus = targetStatus.BoostOf 56 } 57 58 boostable, err := p.filter.StatusBoostable(ctx, requestingAccount, targetStatus) 59 if err != nil { 60 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is boostable: %s", targetStatus.ID, err)) 61 } else if !boostable { 62 return nil, gtserror.NewErrorNotFound(errors.New("status is not boostable")) 63 } 64 65 // it's visible! it's boostable! so let's boost the FUCK out of it 66 boostWrapperStatus, err := p.tc.StatusToBoost(ctx, targetStatus, requestingAccount) 67 if err != nil { 68 return nil, gtserror.NewErrorInternalError(err) 69 } 70 71 boostWrapperStatus.CreatedWithApplicationID = application.ID 72 boostWrapperStatus.BoostOfAccount = targetStatus.Account 73 74 // put the boost in the database 75 if err := p.state.DB.PutStatus(ctx, boostWrapperStatus); err != nil { 76 return nil, gtserror.NewErrorInternalError(err) 77 } 78 79 // send it back to the processor for async processing 80 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 81 APObjectType: ap.ActivityAnnounce, 82 APActivityType: ap.ActivityCreate, 83 GTSModel: boostWrapperStatus, 84 OriginAccount: requestingAccount, 85 TargetAccount: targetStatus.Account, 86 }) 87 88 return p.apiStatus(ctx, boostWrapperStatus, requestingAccount) 89 } 90 91 // BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well. 92 func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { 93 targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID) 94 if err != nil { 95 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) 96 } 97 if targetStatus.Account == nil { 98 return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) 99 } 100 101 visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus) 102 if err != nil { 103 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) 104 } 105 if !visible { 106 return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) 107 } 108 109 // check if we actually have a boost for this status 110 var toUnboost bool 111 112 gtsBoost := >smodel.Status{} 113 where := []db.Where{ 114 { 115 Key: "boost_of_id", 116 Value: targetStatusID, 117 }, 118 { 119 Key: "account_id", 120 Value: requestingAccount.ID, 121 }, 122 } 123 err = p.state.DB.GetWhere(ctx, where, gtsBoost) 124 if err == nil { 125 // we have a boost 126 toUnboost = true 127 } 128 129 if err != nil { 130 // something went wrong in the db finding the boost 131 if err != db.ErrNoEntries { 132 return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err)) 133 } 134 // we just don't have a boost 135 toUnboost = false 136 } 137 138 if toUnboost { 139 // pin some stuff onto the boost while we have it out of the db 140 gtsBoost.Account = requestingAccount 141 gtsBoost.BoostOf = targetStatus 142 gtsBoost.BoostOfAccount = targetStatus.Account 143 gtsBoost.BoostOf.Account = targetStatus.Account 144 145 // send it back to the processor for async processing 146 p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ 147 APObjectType: ap.ActivityAnnounce, 148 APActivityType: ap.ActivityUndo, 149 GTSModel: gtsBoost, 150 OriginAccount: requestingAccount, 151 TargetAccount: targetStatus.Account, 152 }) 153 } 154 155 return p.apiStatus(ctx, targetStatus, requestingAccount) 156 } 157 158 // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. 159 func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { 160 targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID) 161 if err != nil { 162 wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err) 163 if !errors.Is(err, db.ErrNoEntries) { 164 return nil, gtserror.NewErrorInternalError(wrapped) 165 } 166 return nil, gtserror.NewErrorNotFound(wrapped) 167 } 168 169 if boostOfID := targetStatus.BoostOfID; boostOfID != "" { 170 // the target status is a boost wrapper, redirect this request to the status it boosts 171 boostedStatus, err := p.state.DB.GetStatusByID(ctx, boostOfID) 172 if err != nil { 173 wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err) 174 if !errors.Is(err, db.ErrNoEntries) { 175 return nil, gtserror.NewErrorInternalError(wrapped) 176 } 177 return nil, gtserror.NewErrorNotFound(wrapped) 178 } 179 targetStatus = boostedStatus 180 } 181 182 visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus) 183 if err != nil { 184 err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err) 185 return nil, gtserror.NewErrorNotFound(err) 186 } 187 if !visible { 188 err = errors.New("BoostedBy: status is not visible") 189 return nil, gtserror.NewErrorNotFound(err) 190 } 191 192 statusReblogs, err := p.state.DB.GetStatusReblogs(ctx, targetStatus) 193 if err != nil { 194 err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err) 195 return nil, gtserror.NewErrorNotFound(err) 196 } 197 198 // filter account IDs so the user doesn't see accounts they blocked or which blocked them 199 accountIDs := make([]string, 0, len(statusReblogs)) 200 for _, s := range statusReblogs { 201 blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, s.AccountID) 202 if err != nil { 203 err = fmt.Errorf("BoostedBy: error checking blocks: %s", err) 204 return nil, gtserror.NewErrorNotFound(err) 205 } 206 if !blocked { 207 accountIDs = append(accountIDs, s.AccountID) 208 } 209 } 210 211 // TODO: filter other things here? suspended? muted? silenced? 212 213 // fetch accounts + create their API representations 214 apiAccounts := make([]*apimodel.Account, 0, len(accountIDs)) 215 for _, accountID := range accountIDs { 216 account, err := p.state.DB.GetAccountByID(ctx, accountID) 217 if err != nil { 218 wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err) 219 if !errors.Is(err, db.ErrNoEntries) { 220 return nil, gtserror.NewErrorInternalError(wrapped) 221 } 222 return nil, gtserror.NewErrorNotFound(wrapped) 223 } 224 225 apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account) 226 if err != nil { 227 err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err) 228 return nil, gtserror.NewErrorInternalError(err) 229 } 230 apiAccounts = append(apiAccounts, apiAccount) 231 } 232 233 return apiAccounts, nil 234 }