gtsocial-umbx

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

account.go (20102B)


      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 	"strings"
     25 	"time"
     26 
     27 	"github.com/superseriousbusiness/gotosocial/internal/config"
     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/id"
     33 	"github.com/superseriousbusiness/gotosocial/internal/log"
     34 	"github.com/superseriousbusiness/gotosocial/internal/state"
     35 	"github.com/superseriousbusiness/gotosocial/internal/util"
     36 	"github.com/uptrace/bun"
     37 	"github.com/uptrace/bun/dialect"
     38 )
     39 
     40 type accountDB struct {
     41 	conn  *DBConn
     42 	state *state.State
     43 }
     44 
     45 func (a *accountDB) GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, db.Error) {
     46 	return a.getAccount(
     47 		ctx,
     48 		"ID",
     49 		func(account *gtsmodel.Account) error {
     50 			return a.conn.NewSelect().
     51 				Model(account).
     52 				Where("? = ?", bun.Ident("account.id"), id).
     53 				Scan(ctx)
     54 		},
     55 		id,
     56 	)
     57 }
     58 
     59 func (a *accountDB) GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, db.Error) {
     60 	return a.getAccount(
     61 		ctx,
     62 		"URI",
     63 		func(account *gtsmodel.Account) error {
     64 			return a.conn.NewSelect().
     65 				Model(account).
     66 				Where("? = ?", bun.Ident("account.uri"), uri).
     67 				Scan(ctx)
     68 		},
     69 		uri,
     70 	)
     71 }
     72 
     73 func (a *accountDB) GetAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, db.Error) {
     74 	return a.getAccount(
     75 		ctx,
     76 		"URL",
     77 		func(account *gtsmodel.Account) error {
     78 			return a.conn.NewSelect().
     79 				Model(account).
     80 				Where("? = ?", bun.Ident("account.url"), url).
     81 				Scan(ctx)
     82 		},
     83 		url,
     84 	)
     85 }
     86 
     87 func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, db.Error) {
     88 	if domain != "" {
     89 		// Normalize the domain as punycode
     90 		var err error
     91 		domain, err = util.Punify(domain)
     92 		if err != nil {
     93 			return nil, err
     94 		}
     95 	}
     96 
     97 	return a.getAccount(
     98 		ctx,
     99 		"Username.Domain",
    100 		func(account *gtsmodel.Account) error {
    101 			q := a.conn.NewSelect().
    102 				Model(account)
    103 
    104 			if domain != "" {
    105 				q = q.
    106 					Where("LOWER(?) = ?", bun.Ident("account.username"), strings.ToLower(username)).
    107 					Where("? = ?", bun.Ident("account.domain"), domain)
    108 			} else {
    109 				q = q.
    110 					Where("? = ?", bun.Ident("account.username"), strings.ToLower(username)). // usernames on our instance are always lowercase
    111 					Where("? IS NULL", bun.Ident("account.domain"))
    112 			}
    113 
    114 			return q.Scan(ctx)
    115 		},
    116 		username,
    117 		domain,
    118 	)
    119 }
    120 
    121 func (a *accountDB) GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmodel.Account, db.Error) {
    122 	return a.getAccount(
    123 		ctx,
    124 		"PublicKeyURI",
    125 		func(account *gtsmodel.Account) error {
    126 			return a.conn.NewSelect().
    127 				Model(account).
    128 				Where("? = ?", bun.Ident("account.public_key_uri"), id).
    129 				Scan(ctx)
    130 		},
    131 		id,
    132 	)
    133 }
    134 
    135 func (a *accountDB) GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, db.Error) {
    136 	return a.getAccount(
    137 		ctx,
    138 		"InboxURI",
    139 		func(account *gtsmodel.Account) error {
    140 			return a.conn.NewSelect().
    141 				Model(account).
    142 				Where("? = ?", bun.Ident("account.inbox_uri"), uri).
    143 				Scan(ctx)
    144 		},
    145 		uri,
    146 	)
    147 }
    148 
    149 func (a *accountDB) GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, db.Error) {
    150 	return a.getAccount(
    151 		ctx,
    152 		"OutboxURI",
    153 		func(account *gtsmodel.Account) error {
    154 			return a.conn.NewSelect().
    155 				Model(account).
    156 				Where("? = ?", bun.Ident("account.outbox_uri"), uri).
    157 				Scan(ctx)
    158 		},
    159 		uri,
    160 	)
    161 }
    162 
    163 func (a *accountDB) GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, db.Error) {
    164 	return a.getAccount(
    165 		ctx,
    166 		"FollowersURI",
    167 		func(account *gtsmodel.Account) error {
    168 			return a.conn.NewSelect().
    169 				Model(account).
    170 				Where("? = ?", bun.Ident("account.followers_uri"), uri).
    171 				Scan(ctx)
    172 		},
    173 		uri,
    174 	)
    175 }
    176 
    177 func (a *accountDB) GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, db.Error) {
    178 	return a.getAccount(
    179 		ctx,
    180 		"FollowingURI",
    181 		func(account *gtsmodel.Account) error {
    182 			return a.conn.NewSelect().
    183 				Model(account).
    184 				Where("? = ?", bun.Ident("account.following_uri"), uri).
    185 				Scan(ctx)
    186 		},
    187 		uri,
    188 	)
    189 }
    190 
    191 func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, db.Error) {
    192 	var username string
    193 
    194 	if domain == "" {
    195 		// I.e. our local instance account
    196 		username = config.GetHost()
    197 	} else {
    198 		// A remote instance account
    199 		username = domain
    200 	}
    201 
    202 	return a.GetAccountByUsernameDomain(ctx, username, domain)
    203 }
    204 
    205 func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Account) error, keyParts ...any) (*gtsmodel.Account, db.Error) {
    206 	// Fetch account from database cache with loader callback
    207 	account, err := a.state.Caches.GTS.Account().Load(lookup, func() (*gtsmodel.Account, error) {
    208 		var account gtsmodel.Account
    209 
    210 		// Not cached! Perform database query
    211 		if err := dbQuery(&account); err != nil {
    212 			return nil, a.conn.ProcessError(err)
    213 		}
    214 
    215 		return &account, nil
    216 	}, keyParts...)
    217 	if err != nil {
    218 		return nil, err
    219 	}
    220 
    221 	if gtscontext.Barebones(ctx) {
    222 		// no need to fully populate.
    223 		return account, nil
    224 	}
    225 
    226 	// Further populate the account fields where applicable.
    227 	if err := a.PopulateAccount(ctx, account); err != nil {
    228 		return nil, err
    229 	}
    230 
    231 	return account, nil
    232 }
    233 
    234 func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Account) error {
    235 	var (
    236 		err  error
    237 		errs = make(gtserror.MultiError, 0, 3)
    238 	)
    239 
    240 	if account.AvatarMediaAttachment == nil && account.AvatarMediaAttachmentID != "" {
    241 		// Account avatar attachment is not set, fetch from database.
    242 		account.AvatarMediaAttachment, err = a.state.DB.GetAttachmentByID(
    243 			ctx, // these are already barebones
    244 			account.AvatarMediaAttachmentID,
    245 		)
    246 		if err != nil {
    247 			errs.Append(fmt.Errorf("error populating account avatar: %w", err))
    248 		}
    249 	}
    250 
    251 	if account.HeaderMediaAttachment == nil && account.HeaderMediaAttachmentID != "" {
    252 		// Account header attachment is not set, fetch from database.
    253 		account.HeaderMediaAttachment, err = a.state.DB.GetAttachmentByID(
    254 			ctx, // these are already barebones
    255 			account.HeaderMediaAttachmentID,
    256 		)
    257 		if err != nil {
    258 			errs.Append(fmt.Errorf("error populating account header: %w", err))
    259 		}
    260 	}
    261 
    262 	if !account.EmojisPopulated() {
    263 		// Account emojis are out-of-date with IDs, repopulate.
    264 		account.Emojis, err = a.state.DB.GetEmojisByIDs(
    265 			ctx, // these are already barebones
    266 			account.EmojiIDs,
    267 		)
    268 		if err != nil {
    269 			errs.Append(fmt.Errorf("error populating account emojis: %w", err))
    270 		}
    271 	}
    272 
    273 	return errs.Combine()
    274 }
    275 
    276 func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) db.Error {
    277 	return a.state.Caches.GTS.Account().Store(account, func() error {
    278 		// It is safe to run this database transaction within cache.Store
    279 		// as the cache does not attempt a mutex lock until AFTER hook.
    280 		//
    281 		return a.conn.RunInTx(ctx, func(tx bun.Tx) error {
    282 			// create links between this account and any emojis it uses
    283 			for _, i := range account.EmojiIDs {
    284 				if _, err := tx.NewInsert().Model(&gtsmodel.AccountToEmoji{
    285 					AccountID: account.ID,
    286 					EmojiID:   i,
    287 				}).Exec(ctx); err != nil {
    288 					return err
    289 				}
    290 			}
    291 
    292 			// insert the account
    293 			_, err := tx.NewInsert().Model(account).Exec(ctx)
    294 			return err
    295 		})
    296 	})
    297 }
    298 
    299 func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account, columns ...string) db.Error {
    300 	account.UpdatedAt = time.Now()
    301 	if len(columns) > 0 {
    302 		// If we're updating by column, ensure "updated_at" is included.
    303 		columns = append(columns, "updated_at")
    304 	}
    305 
    306 	return a.state.Caches.GTS.Account().Store(account, func() error {
    307 		// It is safe to run this database transaction within cache.Store
    308 		// as the cache does not attempt a mutex lock until AFTER hook.
    309 		//
    310 		return a.conn.RunInTx(ctx, func(tx bun.Tx) error {
    311 			// create links between this account and any emojis it uses
    312 			// first clear out any old emoji links
    313 			if _, err := tx.
    314 				NewDelete().
    315 				TableExpr("? AS ?", bun.Ident("account_to_emojis"), bun.Ident("account_to_emoji")).
    316 				Where("? = ?", bun.Ident("account_to_emoji.account_id"), account.ID).
    317 				Exec(ctx); err != nil {
    318 				return err
    319 			}
    320 
    321 			// now populate new emoji links
    322 			for _, i := range account.EmojiIDs {
    323 				if _, err := tx.
    324 					NewInsert().
    325 					Model(&gtsmodel.AccountToEmoji{
    326 						AccountID: account.ID,
    327 						EmojiID:   i,
    328 					}).Exec(ctx); err != nil {
    329 					return err
    330 				}
    331 			}
    332 
    333 			// update the account
    334 			_, err := tx.NewUpdate().
    335 				Model(account).
    336 				Where("? = ?", bun.Ident("account.id"), account.ID).
    337 				Column(columns...).
    338 				Exec(ctx)
    339 			return err
    340 		})
    341 	})
    342 }
    343 
    344 func (a *accountDB) DeleteAccount(ctx context.Context, id string) db.Error {
    345 	defer a.state.Caches.GTS.Account().Invalidate("ID", id)
    346 
    347 	// Load account into cache before attempting a delete,
    348 	// as we need it cached in order to trigger the invalidate
    349 	// callback. This in turn invalidates others.
    350 	_, err := a.GetAccountByID(gtscontext.SetBarebones(ctx), id)
    351 	if err != nil && !errors.Is(err, db.ErrNoEntries) {
    352 		// NOTE: even if db.ErrNoEntries is returned, we
    353 		// still run the below transaction to ensure related
    354 		// objects are appropriately deleted.
    355 		return err
    356 	}
    357 
    358 	return a.conn.RunInTx(ctx, func(tx bun.Tx) error {
    359 		// clear out any emoji links
    360 		if _, err := tx.
    361 			NewDelete().
    362 			TableExpr("? AS ?", bun.Ident("account_to_emojis"), bun.Ident("account_to_emoji")).
    363 			Where("? = ?", bun.Ident("account_to_emoji.account_id"), id).
    364 			Exec(ctx); err != nil {
    365 			return err
    366 		}
    367 
    368 		// delete the account
    369 		_, err := tx.
    370 			NewDelete().
    371 			TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
    372 			Where("? = ?", bun.Ident("account.id"), id).
    373 			Exec(ctx)
    374 		return err
    375 	})
    376 }
    377 
    378 func (a *accountDB) GetAccountLastPosted(ctx context.Context, accountID string, webOnly bool) (time.Time, db.Error) {
    379 	createdAt := time.Time{}
    380 
    381 	q := a.conn.
    382 		NewSelect().
    383 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    384 		Column("status.created_at").
    385 		Where("? = ?", bun.Ident("status.account_id"), accountID).
    386 		Order("status.id DESC").
    387 		Limit(1)
    388 
    389 	if webOnly {
    390 		q = q.
    391 			WhereGroup(" AND ", whereEmptyOrNull("status.in_reply_to_uri")).
    392 			WhereGroup(" AND ", whereEmptyOrNull("status.boost_of_id")).
    393 			Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic).
    394 			Where("? = ?", bun.Ident("status.federated"), true)
    395 	}
    396 
    397 	if err := q.Scan(ctx, &createdAt); err != nil {
    398 		return time.Time{}, a.conn.ProcessError(err)
    399 	}
    400 	return createdAt, nil
    401 }
    402 
    403 func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.Error {
    404 	if *mediaAttachment.Avatar && *mediaAttachment.Header {
    405 		return errors.New("one media attachment cannot be both header and avatar")
    406 	}
    407 
    408 	var column bun.Ident
    409 	switch {
    410 	case *mediaAttachment.Avatar:
    411 		column = bun.Ident("account.avatar_media_attachment_id")
    412 	case *mediaAttachment.Header:
    413 		column = bun.Ident("account.header_media_attachment_id")
    414 	default:
    415 		return errors.New("given media attachment was neither a header nor an avatar")
    416 	}
    417 
    418 	// TODO: there are probably more side effects here that need to be handled
    419 	if _, err := a.conn.
    420 		NewInsert().
    421 		Model(mediaAttachment).
    422 		Exec(ctx); err != nil {
    423 		return a.conn.ProcessError(err)
    424 	}
    425 
    426 	if _, err := a.conn.
    427 		NewUpdate().
    428 		TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
    429 		Set("? = ?", column, mediaAttachment.ID).
    430 		Where("? = ?", bun.Ident("account.id"), accountID).
    431 		Exec(ctx); err != nil {
    432 		return a.conn.ProcessError(err)
    433 	}
    434 
    435 	return nil
    436 }
    437 
    438 func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, db.Error) {
    439 	account, err := a.GetAccountByUsernameDomain(ctx, username, "")
    440 	if err != nil {
    441 		return "", err
    442 	}
    443 
    444 	return account.CustomCSS, nil
    445 }
    446 
    447 func (a *accountDB) GetAccountFaves(ctx context.Context, accountID string) ([]*gtsmodel.StatusFave, db.Error) {
    448 	faves := new([]*gtsmodel.StatusFave)
    449 
    450 	if err := a.conn.
    451 		NewSelect().
    452 		Model(faves).
    453 		Where("? = ?", bun.Ident("status_fave.account_id"), accountID).
    454 		Scan(ctx); err != nil {
    455 		return nil, a.conn.ProcessError(err)
    456 	}
    457 
    458 	return *faves, nil
    459 }
    460 
    461 func (a *accountDB) CountAccountStatuses(ctx context.Context, accountID string) (int, db.Error) {
    462 	return a.conn.
    463 		NewSelect().
    464 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    465 		Where("? = ?", bun.Ident("status.account_id"), accountID).
    466 		Count(ctx)
    467 }
    468 
    469 func (a *accountDB) CountAccountPinned(ctx context.Context, accountID string) (int, db.Error) {
    470 	return a.conn.
    471 		NewSelect().
    472 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    473 		Where("? = ?", bun.Ident("status.account_id"), accountID).
    474 		Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
    475 		Count(ctx)
    476 }
    477 
    478 func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) {
    479 	// Ensure reasonable
    480 	if limit < 0 {
    481 		limit = 0
    482 	}
    483 
    484 	// Make educated guess for slice size
    485 	var (
    486 		statusIDs   = make([]string, 0, limit)
    487 		frontToBack = true
    488 	)
    489 
    490 	q := a.conn.
    491 		NewSelect().
    492 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    493 		// Select only IDs from table
    494 		Column("status.id").
    495 		Where("? = ?", bun.Ident("status.account_id"), accountID)
    496 
    497 	if excludeReplies {
    498 		q = q.WhereGroup(" AND ", func(*bun.SelectQuery) *bun.SelectQuery {
    499 			return q.
    500 				// Do include self replies (threads), but
    501 				// don't include replies to other people.
    502 				Where("? = ?", bun.Ident("status.in_reply_to_account_id"), accountID).
    503 				WhereOr("? IS NULL", bun.Ident("status.in_reply_to_uri"))
    504 		})
    505 	}
    506 
    507 	if excludeReblogs {
    508 		q = q.Where("? IS NULL", bun.Ident("status.boost_of_id"))
    509 	}
    510 
    511 	if mediaOnly {
    512 		// Attachments are stored as a json object; this
    513 		// implementation differs between SQLite and Postgres,
    514 		// so we have to be thorough to cover all eventualities
    515 		q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
    516 			switch a.conn.Dialect().Name() {
    517 			case dialect.PG:
    518 				return q.
    519 					Where("? IS NOT NULL", bun.Ident("status.attachments")).
    520 					Where("? != '{}'", bun.Ident("status.attachments"))
    521 			case dialect.SQLite:
    522 				return q.
    523 					Where("? IS NOT NULL", bun.Ident("status.attachments")).
    524 					Where("? != ''", bun.Ident("status.attachments")).
    525 					Where("? != 'null'", bun.Ident("status.attachments")).
    526 					Where("? != '{}'", bun.Ident("status.attachments")).
    527 					Where("? != '[]'", bun.Ident("status.attachments"))
    528 			default:
    529 				log.Panic(ctx, "db dialect was neither pg nor sqlite")
    530 				return q
    531 			}
    532 		})
    533 	}
    534 
    535 	if publicOnly {
    536 		q = q.Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic)
    537 	}
    538 
    539 	// return only statuses LOWER (ie., older) than maxID
    540 	if maxID == "" {
    541 		maxID = id.Highest
    542 	}
    543 	q = q.Where("? < ?", bun.Ident("status.id"), maxID)
    544 
    545 	if minID != "" {
    546 		// return only statuses HIGHER (ie., newer) than minID
    547 		q = q.Where("? > ?", bun.Ident("status.id"), minID)
    548 
    549 		// page up
    550 		frontToBack = false
    551 	}
    552 
    553 	if limit > 0 {
    554 		// limit amount of statuses returned
    555 		q = q.Limit(limit)
    556 	}
    557 
    558 	if frontToBack {
    559 		// Page down.
    560 		q = q.Order("status.id DESC")
    561 	} else {
    562 		// Page up.
    563 		q = q.Order("status.id ASC")
    564 	}
    565 
    566 	if err := q.Scan(ctx, &statusIDs); err != nil {
    567 		return nil, a.conn.ProcessError(err)
    568 	}
    569 
    570 	// If we're paging up, we still want statuses
    571 	// to be sorted by ID desc, so reverse ids slice.
    572 	// https://zchee.github.io/golang-wiki/SliceTricks/#reversing
    573 	if !frontToBack {
    574 		for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 {
    575 			statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l]
    576 		}
    577 	}
    578 
    579 	return a.statusesFromIDs(ctx, statusIDs)
    580 }
    581 
    582 func (a *accountDB) GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, db.Error) {
    583 	statusIDs := []string{}
    584 
    585 	q := a.conn.
    586 		NewSelect().
    587 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    588 		Column("status.id").
    589 		Where("? = ?", bun.Ident("status.account_id"), accountID).
    590 		Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
    591 		Order("status.pinned_at DESC")
    592 
    593 	if err := q.Scan(ctx, &statusIDs); err != nil {
    594 		return nil, a.conn.ProcessError(err)
    595 	}
    596 
    597 	return a.statusesFromIDs(ctx, statusIDs)
    598 }
    599 
    600 func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, db.Error) {
    601 	// Ensure reasonable
    602 	if limit < 0 {
    603 		limit = 0
    604 	}
    605 
    606 	// Make educated guess for slice size
    607 	statusIDs := make([]string, 0, limit)
    608 
    609 	q := a.conn.
    610 		NewSelect().
    611 		TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
    612 		// Select only IDs from table
    613 		Column("status.id").
    614 		Where("? = ?", bun.Ident("status.account_id"), accountID).
    615 		// Don't show replies or boosts.
    616 		Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
    617 		Where("? IS NULL", bun.Ident("status.boost_of_id")).
    618 		// Only Public statuses.
    619 		Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic).
    620 		// Don't show local-only statuses on the web view.
    621 		Where("? = ?", bun.Ident("status.federated"), true)
    622 
    623 	// return only statuses LOWER (ie., older) than maxID
    624 	if maxID == "" {
    625 		maxID = id.Highest
    626 	}
    627 	q = q.Where("? < ?", bun.Ident("status.id"), maxID)
    628 
    629 	if limit > 0 {
    630 		// limit amount of statuses returned
    631 		q = q.Limit(limit)
    632 	}
    633 
    634 	if limit > 0 {
    635 		// limit amount of statuses returned
    636 		q = q.Limit(limit)
    637 	}
    638 
    639 	q = q.Order("status.id DESC")
    640 
    641 	if err := q.Scan(ctx, &statusIDs); err != nil {
    642 		return nil, a.conn.ProcessError(err)
    643 	}
    644 
    645 	return a.statusesFromIDs(ctx, statusIDs)
    646 }
    647 
    648 func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) {
    649 	blocks := []*gtsmodel.Block{}
    650 
    651 	fq := a.conn.
    652 		NewSelect().
    653 		Model(&blocks).
    654 		Where("? = ?", bun.Ident("block.account_id"), accountID).
    655 		Relation("TargetAccount").
    656 		Order("block.id DESC")
    657 
    658 	if maxID != "" {
    659 		fq = fq.Where("? < ?", bun.Ident("block.id"), maxID)
    660 	}
    661 
    662 	if sinceID != "" {
    663 		fq = fq.Where("? > ?", bun.Ident("block.id"), sinceID)
    664 	}
    665 
    666 	if limit > 0 {
    667 		fq = fq.Limit(limit)
    668 	}
    669 
    670 	if err := fq.Scan(ctx); err != nil {
    671 		return nil, "", "", a.conn.ProcessError(err)
    672 	}
    673 
    674 	if len(blocks) == 0 {
    675 		return nil, "", "", db.ErrNoEntries
    676 	}
    677 
    678 	accounts := []*gtsmodel.Account{}
    679 	for _, b := range blocks {
    680 		accounts = append(accounts, b.TargetAccount)
    681 	}
    682 
    683 	nextMaxID := blocks[len(blocks)-1].ID
    684 	prevMinID := blocks[0].ID
    685 	return accounts, nextMaxID, prevMinID, nil
    686 }
    687 
    688 func (a *accountDB) statusesFromIDs(ctx context.Context, statusIDs []string) ([]*gtsmodel.Status, db.Error) {
    689 	// Catch case of no statuses early
    690 	if len(statusIDs) == 0 {
    691 		return nil, db.ErrNoEntries
    692 	}
    693 
    694 	// Allocate return slice (will be at most len statusIDS)
    695 	statuses := make([]*gtsmodel.Status, 0, len(statusIDs))
    696 
    697 	for _, id := range statusIDs {
    698 		// Fetch from status from database by ID
    699 		status, err := a.state.DB.GetStatusByID(ctx, id)
    700 		if err != nil {
    701 			log.Errorf(ctx, "error getting status %q: %v", id, err)
    702 			continue
    703 		}
    704 
    705 		// Append to return slice
    706 		statuses = append(statuses, status)
    707 	}
    708 
    709 	return statuses, nil
    710 }