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