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(>smodel.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(>smodel.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 }