gtsocial-umbx

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

status.go (17465B)


      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 	"container/list"
     22 	"context"
     23 	"database/sql"
     24 	"errors"
     25 	"fmt"
     26 	"time"
     27 
     28 	"github.com/superseriousbusiness/gotosocial/internal/db"
     29 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
     30 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     31 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     32 	"github.com/superseriousbusiness/gotosocial/internal/log"
     33 	"github.com/superseriousbusiness/gotosocial/internal/state"
     34 	"github.com/uptrace/bun"
     35 )
     36 
     37 type statusDB struct {
     38 	conn  *DBConn
     39 	state *state.State
     40 }
     41 
     42 func (s *statusDB) newStatusQ(status interface{}) *bun.SelectQuery {
     43 	return s.conn.
     44 		NewSelect().
     45 		Model(status).
     46 		Relation("Tags").
     47 		Relation("CreatedWithApplication")
     48 }
     49 
     50 func (s *statusDB) GetStatusByID(ctx context.Context, id string) (*gtsmodel.Status, db.Error) {
     51 	return s.getStatus(
     52 		ctx,
     53 		"ID",
     54 		func(status *gtsmodel.Status) error {
     55 			return s.newStatusQ(status).Where("? = ?", bun.Ident("status.id"), id).Scan(ctx)
     56 		},
     57 		id,
     58 	)
     59 }
     60 
     61 func (s *statusDB) GetStatuses(ctx context.Context, ids []string) ([]*gtsmodel.Status, db.Error) {
     62 	statuses := make([]*gtsmodel.Status, 0, len(ids))
     63 
     64 	for _, id := range ids {
     65 		// Attempt fetch from DB
     66 		status, err := s.GetStatusByID(ctx, id)
     67 		if err != nil {
     68 			log.Errorf(ctx, "error getting status %q: %v", id, err)
     69 			continue
     70 		}
     71 
     72 		// Append status
     73 		statuses = append(statuses, status)
     74 	}
     75 
     76 	return statuses, nil
     77 }
     78 
     79 func (s *statusDB) GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.Status, db.Error) {
     80 	return s.getStatus(
     81 		ctx,
     82 		"URI",
     83 		func(status *gtsmodel.Status) error {
     84 			return s.newStatusQ(status).Where("? = ?", bun.Ident("status.uri"), uri).Scan(ctx)
     85 		},
     86 		uri,
     87 	)
     88 }
     89 
     90 func (s *statusDB) GetStatusByURL(ctx context.Context, url string) (*gtsmodel.Status, db.Error) {
     91 	return s.getStatus(
     92 		ctx,
     93 		"URL",
     94 		func(status *gtsmodel.Status) error {
     95 			return s.newStatusQ(status).Where("? = ?", bun.Ident("status.url"), url).Scan(ctx)
     96 		},
     97 		url,
     98 	)
     99 }
    100 
    101 func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Status) error, keyParts ...any) (*gtsmodel.Status, db.Error) {
    102 	// Fetch status from database cache with loader callback
    103 	status, err := s.state.Caches.GTS.Status().Load(lookup, func() (*gtsmodel.Status, error) {
    104 		var status gtsmodel.Status
    105 
    106 		// Not cached! Perform database query.
    107 		if err := dbQuery(&status); err != nil {
    108 			return nil, s.conn.ProcessError(err)
    109 		}
    110 
    111 		return &status, nil
    112 	}, keyParts...)
    113 	if err != nil {
    114 		return nil, err
    115 	}
    116 
    117 	if gtscontext.Barebones(ctx) {
    118 		// no need to fully populate.
    119 		return status, nil
    120 	}
    121 
    122 	// Further populate the status fields where applicable.
    123 	if err := s.PopulateStatus(ctx, status); err != nil {
    124 		return nil, err
    125 	}
    126 
    127 	return status, nil
    128 }
    129 
    130 func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) error {
    131 	var (
    132 		err  error
    133 		errs = make(gtserror.MultiError, 0, 9)
    134 	)
    135 
    136 	if status.Account == nil {
    137 		// Status author is not set, fetch from database.
    138 		status.Account, err = s.state.DB.GetAccountByID(
    139 			gtscontext.SetBarebones(ctx),
    140 			status.AccountID,
    141 		)
    142 		if err != nil {
    143 			errs.Append(fmt.Errorf("error populating status author: %w", err))
    144 		}
    145 	}
    146 
    147 	if status.InReplyToID != "" && status.InReplyTo == nil {
    148 		// Status parent is not set, fetch from database.
    149 		status.InReplyTo, err = s.GetStatusByID(
    150 			gtscontext.SetBarebones(ctx),
    151 			status.InReplyToID,
    152 		)
    153 		if err != nil {
    154 			errs.Append(fmt.Errorf("error populating status parent: %w", err))
    155 		}
    156 	}
    157 
    158 	if status.InReplyToID != "" {
    159 		if status.InReplyTo == nil {
    160 			// Status parent is not set, fetch from database.
    161 			status.InReplyTo, err = s.GetStatusByID(
    162 				gtscontext.SetBarebones(ctx),
    163 				status.InReplyToID,
    164 			)
    165 			if err != nil {
    166 				errs.Append(fmt.Errorf("error populating status parent: %w", err))
    167 			}
    168 		}
    169 
    170 		if status.InReplyToAccount == nil {
    171 			// Status parent author is not set, fetch from database.
    172 			status.InReplyToAccount, err = s.state.DB.GetAccountByID(
    173 				gtscontext.SetBarebones(ctx),
    174 				status.InReplyToAccountID,
    175 			)
    176 			if err != nil {
    177 				errs.Append(fmt.Errorf("error populating status parent author: %w", err))
    178 			}
    179 		}
    180 	}
    181 
    182 	if status.BoostOfID != "" {
    183 		if status.BoostOf == nil {
    184 			// Status boost is not set, fetch from database.
    185 			status.BoostOf, err = s.GetStatusByID(
    186 				gtscontext.SetBarebones(ctx),
    187 				status.BoostOfID,
    188 			)
    189 			if err != nil {
    190 				errs.Append(fmt.Errorf("error populating status boost: %w", err))
    191 			}
    192 		}
    193 
    194 		if status.BoostOfAccount == nil {
    195 			// Status boost author is not set, fetch from database.
    196 			status.BoostOfAccount, err = s.state.DB.GetAccountByID(
    197 				gtscontext.SetBarebones(ctx),
    198 				status.BoostOfAccountID,
    199 			)
    200 			if err != nil {
    201 				errs.Append(fmt.Errorf("error populating status boost author: %w", err))
    202 			}
    203 		}
    204 	}
    205 
    206 	if !status.AttachmentsPopulated() {
    207 		// Status attachments are out-of-date with IDs, repopulate.
    208 		status.Attachments, err = s.state.DB.GetAttachmentsByIDs(
    209 			ctx, // these are already barebones
    210 			status.AttachmentIDs,
    211 		)
    212 		if err != nil {
    213 			errs.Append(fmt.Errorf("error populating status attachments: %w", err))
    214 		}
    215 	}
    216 
    217 	// TODO: once we don't fetch using relations.
    218 	// if !status.TagsPopulated() {
    219 	// }
    220 
    221 	if !status.MentionsPopulated() {
    222 		// Status mentions are out-of-date with IDs, repopulate.
    223 		status.Mentions, err = s.state.DB.GetMentions(
    224 			ctx, // leave fully populated for now
    225 			status.MentionIDs,
    226 		)
    227 		if err != nil {
    228 			errs.Append(fmt.Errorf("error populating status mentions: %w", err))
    229 		}
    230 	}
    231 
    232 	if !status.EmojisPopulated() {
    233 		// Status emojis are out-of-date with IDs, repopulate.
    234 		status.Emojis, err = s.state.DB.GetEmojisByIDs(
    235 			ctx, // these are already barebones
    236 			status.EmojiIDs,
    237 		)
    238 		if err != nil {
    239 			errs.Append(fmt.Errorf("error populating status emojis: %w", err))
    240 		}
    241 	}
    242 
    243 	return errs.Combine()
    244 }
    245 
    246 func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Error {
    247 	return s.state.Caches.GTS.Status().Store(status, func() error {
    248 		// It is safe to run this database transaction within cache.Store
    249 		// as the cache does not attempt a mutex lock until AFTER hook.
    250 		//
    251 		return s.conn.RunInTx(ctx, func(tx bun.Tx) error {
    252 			// create links between this status and any emojis it uses
    253 			for _, i := range status.EmojiIDs {
    254 				if _, err := tx.
    255 					NewInsert().
    256 					Model(&gtsmodel.StatusToEmoji{
    257 						StatusID: status.ID,
    258 						EmojiID:  i,
    259 					}).
    260 					On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")).
    261 					Exec(ctx); err != nil {
    262 					err = s.conn.ProcessError(err)
    263 					if !errors.Is(err, db.ErrAlreadyExists) {
    264 						return err
    265 					}
    266 				}
    267 			}
    268 
    269 			// create links between this status and any tags it uses
    270 			for _, i := range status.TagIDs {
    271 				if _, err := tx.
    272 					NewInsert().
    273 					Model(&gtsmodel.StatusToTag{
    274 						StatusID: status.ID,
    275 						TagID:    i,
    276 					}).
    277 					On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")).
    278 					Exec(ctx); err != nil {
    279 					err = s.conn.ProcessError(err)
    280 					if !errors.Is(err, db.ErrAlreadyExists) {
    281 						return err
    282 					}
    283 				}
    284 			}
    285 
    286 			// change the status ID of the media attachments to the new status
    287 			for _, a := range status.Attachments {
    288 				a.StatusID = status.ID
    289 				a.UpdatedAt = time.Now()
    290 				if _, err := tx.
    291 					NewUpdate().
    292 					Model(a).
    293 					Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
    294 					Exec(ctx); err != nil {
    295 					err = s.conn.ProcessError(err)
    296 					if !errors.Is(err, db.ErrAlreadyExists) {
    297 						return err
    298 					}
    299 				}
    300 			}
    301 
    302 			// Finally, insert the status
    303 			_, err := tx.NewInsert().Model(status).Exec(ctx)
    304 			return err
    305 		})
    306 	})
    307 }
    308 
    309 func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) db.Error {
    310 	status.UpdatedAt = time.Now()
    311 	if len(columns) > 0 {
    312 		// If we're updating by column, ensure "updated_at" is included.
    313 		columns = append(columns, "updated_at")
    314 	}
    315 
    316 	return s.state.Caches.GTS.Status().Store(status, func() error {
    317 		// It is safe to run this database transaction within cache.Store
    318 		// as the cache does not attempt a mutex lock until AFTER hook.
    319 		//
    320 		return s.conn.RunInTx(ctx, func(tx bun.Tx) error {
    321 			// create links between this status and any emojis it uses
    322 			for _, i := range status.EmojiIDs {
    323 				if _, err := tx.
    324 					NewInsert().
    325 					Model(&gtsmodel.StatusToEmoji{
    326 						StatusID: status.ID,
    327 						EmojiID:  i,
    328 					}).
    329 					On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("emoji_id")).
    330 					Exec(ctx); err != nil {
    331 					err = s.conn.ProcessError(err)
    332 					if !errors.Is(err, db.ErrAlreadyExists) {
    333 						return err
    334 					}
    335 				}
    336 			}
    337 
    338 			// create links between this status and any tags it uses
    339 			for _, i := range status.TagIDs {
    340 				if _, err := tx.
    341 					NewInsert().
    342 					Model(&gtsmodel.StatusToTag{
    343 						StatusID: status.ID,
    344 						TagID:    i,
    345 					}).
    346 					On("CONFLICT (?, ?) DO NOTHING", bun.Ident("status_id"), bun.Ident("tag_id")).
    347 					Exec(ctx); err != nil {
    348 					err = s.conn.ProcessError(err)
    349 					if !errors.Is(err, db.ErrAlreadyExists) {
    350 						return err
    351 					}
    352 				}
    353 			}
    354 
    355 			// change the status ID of the media attachments to the new status
    356 			for _, a := range status.Attachments {
    357 				a.StatusID = status.ID
    358 				a.UpdatedAt = time.Now()
    359 				if _, err := tx.
    360 					NewUpdate().
    361 					Model(a).
    362 					Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
    363 					Exec(ctx); err != nil {
    364 					err = s.conn.ProcessError(err)
    365 					if !errors.Is(err, db.ErrAlreadyExists) {
    366 						return err
    367 					}
    368 				}
    369 			}
    370 
    371 			// Finally, update the status
    372 			_, err := tx.
    373 				NewUpdate().
    374 				Model(status).
    375 				Column(columns...).
    376 				Where("? = ?", bun.Ident("status.id"), status.ID).
    377 				Exec(ctx)
    378 			return err
    379 		})
    380 	})
    381 }
    382 
    383 func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) db.Error {
    384 	defer s.state.Caches.GTS.Status().Invalidate("ID", id)
    385 
    386 	// Load status into cache before attempting a delete,
    387 	// as we need it cached in order to trigger the invalidate
    388 	// callback. This in turn invalidates others.
    389 	_, err := s.GetStatusByID(
    390 		gtscontext.SetBarebones(ctx),
    391 		id,
    392 	)
    393 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    394 		// NOTE: even if db.ErrNoEntries is returned, we
    395 		// still run the below transaction to ensure related
    396 		// objects are appropriately deleted.
    397 		return err
    398 	}
    399 
    400 	return s.conn.RunInTx(ctx, func(tx bun.Tx) error {
    401 		// delete links between this status and any emojis it uses
    402 		if _, err := tx.
    403 			NewDelete().
    404 			TableExpr("? AS ?", bun.Ident("status_to_emojis"), bun.Ident("status_to_emoji")).
    405 			Where("? = ?", bun.Ident("status_to_emoji.status_id"), id).
    406 			Exec(ctx); err != nil {
    407 			return err
    408 		}
    409 
    410 		// delete links between this status and any tags it uses
    411 		if _, err := tx.
    412 			NewDelete().
    413 			TableExpr("? AS ?", bun.Ident("status_to_tags"), bun.Ident("status_to_tag")).
    414 			Where("? = ?", bun.Ident("status_to_tag.status_id"), id).
    415 			Exec(ctx); err != nil {
    416 			return err
    417 		}
    418 
    419 		// delete the status itself
    420 		if _, err := tx.
    421 			NewDelete().
    422 			TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    423 			Where("? = ?", bun.Ident("status.id"), id).
    424 			Exec(ctx); err != nil {
    425 			return err
    426 		}
    427 
    428 		return nil
    429 	})
    430 }
    431 
    432 func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) {
    433 	if onlyDirect {
    434 		// Only want the direct parent, no further than first level
    435 		parent, err := s.GetStatusByID(ctx, status.InReplyToID)
    436 		if err != nil {
    437 			return nil, err
    438 		}
    439 		return []*gtsmodel.Status{parent}, nil
    440 	}
    441 
    442 	var parents []*gtsmodel.Status
    443 
    444 	for id := status.InReplyToID; id != ""; {
    445 		parent, err := s.GetStatusByID(ctx, id)
    446 		if err != nil {
    447 			return nil, err
    448 		}
    449 
    450 		// Append parent to slice
    451 		parents = append(parents, parent)
    452 
    453 		// Set the next parent ID
    454 		id = parent.InReplyToID
    455 	}
    456 
    457 	return parents, nil
    458 }
    459 
    460 func (s *statusDB) GetStatusChildren(ctx context.Context, status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.Error) {
    461 	foundStatuses := &list.List{}
    462 	foundStatuses.PushFront(status)
    463 	s.statusChildren(ctx, status, foundStatuses, onlyDirect, minID)
    464 
    465 	children := []*gtsmodel.Status{}
    466 	for e := foundStatuses.Front(); e != nil; e = e.Next() {
    467 		// only append children, not the overall parent status
    468 		entry, ok := e.Value.(*gtsmodel.Status)
    469 		if !ok {
    470 			log.Panic(ctx, "found status could not be asserted to *gtsmodel.Status")
    471 		}
    472 
    473 		if entry.ID != status.ID {
    474 			children = append(children, entry)
    475 		}
    476 	}
    477 
    478 	return children, nil
    479 }
    480 
    481 func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) {
    482 	var childIDs []string
    483 
    484 	q := s.conn.
    485 		NewSelect().
    486 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    487 		Column("status.id").
    488 		Where("? = ?", bun.Ident("status.in_reply_to_id"), status.ID)
    489 	if minID != "" {
    490 		q = q.Where("? > ?", bun.Ident("status.id"), minID)
    491 	}
    492 
    493 	if err := q.Scan(ctx, &childIDs); err != nil {
    494 		if err != sql.ErrNoRows {
    495 			log.Errorf(ctx, "error getting children for %q: %v", status.ID, err)
    496 		}
    497 		return
    498 	}
    499 
    500 	for _, id := range childIDs {
    501 		// Fetch child with ID from database
    502 		child, err := s.GetStatusByID(ctx, id)
    503 		if err != nil {
    504 			log.Errorf(ctx, "error getting child status %q: %v", id, err)
    505 			continue
    506 		}
    507 
    508 	insertLoop:
    509 		for e := foundStatuses.Front(); e != nil; e = e.Next() {
    510 			entry, ok := e.Value.(*gtsmodel.Status)
    511 			if !ok {
    512 				log.Panic(ctx, "found status could not be asserted to *gtsmodel.Status")
    513 			}
    514 
    515 			if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID {
    516 				foundStatuses.InsertAfter(child, e)
    517 				break insertLoop
    518 			}
    519 		}
    520 
    521 		// if we're not only looking for direct children of status, then do the same children-finding
    522 		// operation for the found child status too.
    523 		if !onlyDirect {
    524 			s.statusChildren(ctx, child, foundStatuses, false, minID)
    525 		}
    526 	}
    527 }
    528 
    529 func (s *statusDB) CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, db.Error) {
    530 	return s.conn.
    531 		NewSelect().
    532 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    533 		Where("? = ?", bun.Ident("status.in_reply_to_id"), status.ID).
    534 		Count(ctx)
    535 }
    536 
    537 func (s *statusDB) CountStatusReblogs(ctx context.Context, status *gtsmodel.Status) (int, db.Error) {
    538 	return s.conn.
    539 		NewSelect().
    540 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    541 		Where("? = ?", bun.Ident("status.boost_of_id"), status.ID).
    542 		Count(ctx)
    543 }
    544 
    545 func (s *statusDB) CountStatusFaves(ctx context.Context, status *gtsmodel.Status) (int, db.Error) {
    546 	return s.conn.
    547 		NewSelect().
    548 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
    549 		Where("? = ?", bun.Ident("status_fave.status_id"), status.ID).
    550 		Count(ctx)
    551 }
    552 
    553 func (s *statusDB) IsStatusFavedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, db.Error) {
    554 	q := s.conn.
    555 		NewSelect().
    556 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
    557 		Where("? = ?", bun.Ident("status_fave.status_id"), status.ID).
    558 		Where("? = ?", bun.Ident("status_fave.account_id"), accountID)
    559 
    560 	return s.conn.Exists(ctx, q)
    561 }
    562 
    563 func (s *statusDB) IsStatusRebloggedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, db.Error) {
    564 	q := s.conn.
    565 		NewSelect().
    566 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    567 		Where("? = ?", bun.Ident("status.boost_of_id"), status.ID).
    568 		Where("? = ?", bun.Ident("status.account_id"), accountID)
    569 
    570 	return s.conn.Exists(ctx, q)
    571 }
    572 
    573 func (s *statusDB) IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, db.Error) {
    574 	q := s.conn.
    575 		NewSelect().
    576 		TableExpr("? AS ?", bun.Ident("status_mutes"), bun.Ident("status_mute")).
    577 		Where("? = ?", bun.Ident("status_mute.status_id"), status.ID).
    578 		Where("? = ?", bun.Ident("status_mute.account_id"), accountID)
    579 
    580 	return s.conn.Exists(ctx, q)
    581 }
    582 
    583 func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, db.Error) {
    584 	q := s.conn.
    585 		NewSelect().
    586 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
    587 		Where("? = ?", bun.Ident("status_bookmark.status_id"), status.ID).
    588 		Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID)
    589 
    590 	return s.conn.Exists(ctx, q)
    591 }
    592 
    593 func (s *statusDB) GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) {
    594 	reblogs := []*gtsmodel.Status{}
    595 
    596 	q := s.
    597 		newStatusQ(&reblogs).
    598 		Where("? = ?", bun.Ident("status.boost_of_id"), status.ID)
    599 
    600 	if err := q.Scan(ctx); err != nil {
    601 		return nil, s.conn.ProcessError(err)
    602 	}
    603 	return reblogs, nil
    604 }