gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }