statusfave.go (8533B)
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 bundb 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 25 "github.com/superseriousbusiness/gotosocial/internal/db" 26 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 27 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 28 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 29 "github.com/superseriousbusiness/gotosocial/internal/log" 30 "github.com/superseriousbusiness/gotosocial/internal/state" 31 "github.com/uptrace/bun" 32 ) 33 34 type statusFaveDB struct { 35 conn *DBConn 36 state *state.State 37 } 38 39 func (s *statusFaveDB) GetStatusFave(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, db.Error) { 40 return s.getStatusFave( 41 ctx, 42 "AccountID.StatusID", 43 func(fave *gtsmodel.StatusFave) error { 44 return s.conn. 45 NewSelect(). 46 Model(fave). 47 Where("? = ?", bun.Ident("account_id"), accountID). 48 Where("? = ?", bun.Ident("status_id"), statusID). 49 Scan(ctx) 50 }, 51 accountID, 52 statusID, 53 ) 54 } 55 56 func (s *statusFaveDB) GetStatusFaveByID(ctx context.Context, id string) (*gtsmodel.StatusFave, db.Error) { 57 return s.getStatusFave( 58 ctx, 59 "ID", 60 func(fave *gtsmodel.StatusFave) error { 61 return s.conn. 62 NewSelect(). 63 Model(fave). 64 Where("? = ?", bun.Ident("id"), id). 65 Scan(ctx) 66 }, 67 id, 68 ) 69 } 70 71 func (s *statusFaveDB) getStatusFave(ctx context.Context, lookup string, dbQuery func(*gtsmodel.StatusFave) error, keyParts ...any) (*gtsmodel.StatusFave, error) { 72 // Fetch status fave from database cache with loader callback 73 fave, err := s.state.Caches.GTS.StatusFave().Load(lookup, func() (*gtsmodel.StatusFave, error) { 74 var fave gtsmodel.StatusFave 75 76 // Not cached! Perform database query. 77 if err := dbQuery(&fave); err != nil { 78 return nil, s.conn.ProcessError(err) 79 } 80 81 return &fave, nil 82 }, keyParts...) 83 if err != nil { 84 return nil, err 85 } 86 87 if gtscontext.Barebones(ctx) { 88 // no need to fully populate. 89 return fave, nil 90 } 91 92 // Fetch the status fave author account. 93 fave.Account, err = s.state.DB.GetAccountByID( 94 gtscontext.SetBarebones(ctx), 95 fave.AccountID, 96 ) 97 if err != nil { 98 return nil, fmt.Errorf("error getting status fave account %q: %w", fave.AccountID, err) 99 } 100 101 // Fetch the status fave target account. 102 fave.TargetAccount, err = s.state.DB.GetAccountByID( 103 gtscontext.SetBarebones(ctx), 104 fave.TargetAccountID, 105 ) 106 if err != nil { 107 return nil, fmt.Errorf("error getting status fave target account %q: %w", fave.TargetAccountID, err) 108 } 109 110 // Fetch the status fave target status. 111 fave.Status, err = s.state.DB.GetStatusByID( 112 gtscontext.SetBarebones(ctx), 113 fave.StatusID, 114 ) 115 if err != nil { 116 return nil, fmt.Errorf("error getting status fave status %q: %w", fave.StatusID, err) 117 } 118 119 return fave, nil 120 } 121 122 func (s *statusFaveDB) GetStatusFavesForStatus(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, db.Error) { 123 ids := []string{} 124 125 if err := s.conn. 126 NewSelect(). 127 Table("status_faves"). 128 Column("id"). 129 Where("? = ?", bun.Ident("status_id"), statusID). 130 Scan(ctx, &ids); err != nil { 131 return nil, s.conn.ProcessError(err) 132 } 133 134 faves := make([]*gtsmodel.StatusFave, 0, len(ids)) 135 136 for _, id := range ids { 137 fave, err := s.GetStatusFaveByID(ctx, id) 138 if err != nil { 139 log.Errorf(ctx, "error getting status fave %q: %v", id, err) 140 continue 141 } 142 143 faves = append(faves, fave) 144 } 145 146 return faves, nil 147 } 148 149 func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error { 150 var ( 151 err error 152 errs = make(gtserror.MultiError, 0, 3) 153 ) 154 155 if statusFave.Account == nil { 156 // StatusFave author is not set, fetch from database. 157 statusFave.Account, err = s.state.DB.GetAccountByID( 158 gtscontext.SetBarebones(ctx), 159 statusFave.AccountID, 160 ) 161 if err != nil { 162 errs.Append(fmt.Errorf("error populating status fave author: %w", err)) 163 } 164 } 165 166 if statusFave.TargetAccount == nil { 167 // StatusFave target account is not set, fetch from database. 168 statusFave.TargetAccount, err = s.state.DB.GetAccountByID( 169 gtscontext.SetBarebones(ctx), 170 statusFave.TargetAccountID, 171 ) 172 if err != nil { 173 errs.Append(fmt.Errorf("error populating status fave target account: %w", err)) 174 } 175 } 176 177 if statusFave.Status == nil { 178 // StatusFave status is not set, fetch from database. 179 statusFave.Status, err = s.state.DB.GetStatusByID( 180 gtscontext.SetBarebones(ctx), 181 statusFave.StatusID, 182 ) 183 if err != nil { 184 errs.Append(fmt.Errorf("error populating status fave status: %w", err)) 185 } 186 } 187 188 return errs.Combine() 189 } 190 191 func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) db.Error { 192 return s.state.Caches.GTS.StatusFave().Store(fave, func() error { 193 _, err := s.conn. 194 NewInsert(). 195 Model(fave). 196 Exec(ctx) 197 return s.conn.ProcessError(err) 198 }) 199 } 200 201 func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) db.Error { 202 defer s.state.Caches.GTS.StatusFave().Invalidate("ID", id) 203 204 // Load fave into cache before attempting a delete, 205 // as we need it cached in order to trigger the invalidate 206 // callback. This in turn invalidates others. 207 _, err := s.GetStatusFaveByID(gtscontext.SetBarebones(ctx), id) 208 if err != nil { 209 if errors.Is(err, db.ErrNoEntries) { 210 // not an issue. 211 err = nil 212 } 213 return err 214 } 215 216 // Finally delete fave from DB. 217 _, err = s.conn.NewDelete(). 218 Table("status_faves"). 219 Where("? = ?", bun.Ident("id"), id). 220 Exec(ctx) 221 return s.conn.ProcessError(err) 222 } 223 224 func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) db.Error { 225 if targetAccountID == "" && originAccountID == "" { 226 return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set") 227 } 228 229 var faveIDs []string 230 231 q := s.conn. 232 NewSelect(). 233 Column("id"). 234 Table("status_faves") 235 236 if targetAccountID != "" { 237 q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID) 238 } 239 240 if originAccountID != "" { 241 q = q.Where("? = ?", bun.Ident("account_id"), originAccountID) 242 } 243 244 if _, err := q.Exec(ctx, &faveIDs); err != nil { 245 return s.conn.ProcessError(err) 246 } 247 248 defer func() { 249 // Invalidate all IDs on return. 250 for _, id := range faveIDs { 251 s.state.Caches.GTS.StatusFave().Invalidate("ID", id) 252 } 253 }() 254 255 // Load all faves into cache, this *really* isn't great 256 // but it is the only way we can ensure we invalidate all 257 // related caches correctly (e.g. visibility). 258 for _, id := range faveIDs { 259 _, err := s.GetStatusFaveByID(ctx, id) 260 if err != nil && !errors.Is(err, db.ErrNoEntries) { 261 return err 262 } 263 } 264 265 // Finally delete all from DB. 266 _, err := s.conn.NewDelete(). 267 Table("status_faves"). 268 Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)). 269 Exec(ctx) 270 return s.conn.ProcessError(err) 271 } 272 273 func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) db.Error { 274 // Capture fave IDs in a RETURNING statement. 275 var faveIDs []string 276 277 q := s.conn. 278 NewSelect(). 279 Column("id"). 280 Table("status_faves"). 281 Where("? = ?", bun.Ident("status_id"), statusID) 282 if _, err := q.Exec(ctx, &faveIDs); err != nil { 283 return s.conn.ProcessError(err) 284 } 285 286 defer func() { 287 // Invalidate all IDs on return. 288 for _, id := range faveIDs { 289 s.state.Caches.GTS.StatusFave().Invalidate("ID", id) 290 } 291 }() 292 293 // Load all faves into cache, this *really* isn't great 294 // but it is the only way we can ensure we invalidate all 295 // related caches correctly (e.g. visibility). 296 for _, id := range faveIDs { 297 _, err := s.GetStatusFaveByID(ctx, id) 298 if err != nil && !errors.Is(err, db.ErrNoEntries) { 299 return err 300 } 301 } 302 303 // Finally delete all from DB. 304 _, err := s.conn.NewDelete(). 305 Table("status_faves"). 306 Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)). 307 Exec(ctx) 308 return s.conn.ProcessError(err) 309 }