fromcommon.go (18519B)
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 processing 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 25 "github.com/superseriousbusiness/gotosocial/internal/config" 26 "github.com/superseriousbusiness/gotosocial/internal/db" 27 "github.com/superseriousbusiness/gotosocial/internal/email" 28 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 29 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 30 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 31 "github.com/superseriousbusiness/gotosocial/internal/id" 32 "github.com/superseriousbusiness/gotosocial/internal/log" 33 "github.com/superseriousbusiness/gotosocial/internal/stream" 34 "github.com/superseriousbusiness/gotosocial/internal/timeline" 35 ) 36 37 // timelineAndNotifyStatus processes the given new status and inserts it into 38 // the HOME and LIST timelines of accounts that follow the status author. 39 // 40 // It will also handle notifications for any mentions attached to the account, and 41 // also notifications for any local accounts that want to know when this account posts. 42 func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error { 43 // Ensure status fully populated; including account, mentions, etc. 44 if err := p.state.DB.PopulateStatus(ctx, status); err != nil { 45 return fmt.Errorf("timelineAndNotifyStatus: error populating status with id %s: %w", status.ID, err) 46 } 47 48 // Get local followers of the account that posted the status. 49 follows, err := p.state.DB.GetAccountLocalFollowers(ctx, status.AccountID) 50 if err != nil { 51 return fmt.Errorf("timelineAndNotifyStatus: error getting local followers for account id %s: %w", status.AccountID, err) 52 } 53 54 // If the poster is also local, add a fake entry for them 55 // so they can see their own status in their timeline. 56 if status.Account.IsLocal() { 57 follows = append(follows, >smodel.Follow{ 58 AccountID: status.AccountID, 59 Account: status.Account, 60 Notify: func() *bool { b := false; return &b }(), // Account shouldn't notify itself. 61 ShowReblogs: func() *bool { b := true; return &b }(), // Account should show own reblogs. 62 }) 63 } 64 65 // Timeline the status for each local follower of this account. 66 // This will also handle notifying any followers with notify 67 // set to true on their follow. 68 if err := p.timelineAndNotifyStatusForFollowers(ctx, status, follows); err != nil { 69 return fmt.Errorf("timelineAndNotifyStatus: error timelining status %s for followers: %w", status.ID, err) 70 } 71 72 // Notify each local account that's mentioned by this status. 73 if err := p.notifyStatusMentions(ctx, status); err != nil { 74 return fmt.Errorf("timelineAndNotifyStatus: error notifying status mentions for status %s: %w", status.ID, err) 75 } 76 77 return nil 78 } 79 80 func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, status *gtsmodel.Status, follows []*gtsmodel.Follow) error { 81 var ( 82 errs = make(gtserror.MultiError, 0, len(follows)) 83 boost = status.BoostOfID != "" 84 reply = status.InReplyToURI != "" 85 ) 86 87 for _, follow := range follows { 88 if sr := follow.ShowReblogs; boost && (sr == nil || !*sr) { 89 // This is a boost, but this follower 90 // doesn't want to see those from this 91 // account, so just skip everything. 92 continue 93 } 94 95 // Add status to each list that this follow 96 // is included in, and stream it if applicable. 97 listEntries, err := p.state.DB.GetListEntriesForFollowID( 98 // We only need the list IDs. 99 gtscontext.SetBarebones(ctx), 100 follow.ID, 101 ) 102 if err != nil && !errors.Is(err, db.ErrNoEntries) { 103 errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error list timelining status: %w", err)) 104 continue 105 } 106 107 for _, listEntry := range listEntries { 108 if _, err := p.timelineStatus( 109 ctx, 110 p.state.Timelines.List.IngestOne, 111 listEntry.ListID, // list timelines are keyed by list ID 112 follow.Account, 113 status, 114 stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list 115 ); err != nil { 116 errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error list timelining status: %w", err)) 117 continue 118 } 119 } 120 121 // Add status to home timeline for this 122 // follower, and stream it if applicable. 123 if timelined, err := p.timelineStatus( 124 ctx, 125 p.state.Timelines.Home.IngestOne, 126 follow.AccountID, // home timelines are keyed by account ID 127 follow.Account, 128 status, 129 stream.TimelineHome, 130 ); err != nil { 131 errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error home timelining status: %w", err)) 132 continue 133 } else if !timelined { 134 // Status wasn't added to home tomeline, 135 // so we shouldn't notify it either. 136 continue 137 } 138 139 if n := follow.Notify; n == nil || !*n { 140 // This follower doesn't have notifications 141 // set for this account's new posts, so bail. 142 continue 143 } 144 145 if boost || reply { 146 // Don't notify for boosts or replies. 147 continue 148 } 149 150 // If we reach here, we know: 151 // 152 // - This follower wants to be notified when this account posts. 153 // - This is a top-level post (not a reply). 154 // - This is not a boost of another post. 155 // - The post is visible in this follower's home timeline. 156 // 157 // That means we can officially notify this one. 158 if err := p.notify( 159 ctx, 160 gtsmodel.NotificationStatus, 161 follow.AccountID, 162 status.AccountID, 163 status.ID, 164 ); err != nil { 165 errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error notifying account %s about new status: %w", follow.AccountID, err)) 166 } 167 } 168 169 return errs.Combine() 170 } 171 172 // timelineStatus uses the provided ingest function to put the given 173 // status in a timeline with the given ID, if it's timelineable. 174 // 175 // If the status was inserted into the timeline, true will be returned 176 // + it will also be streamed to the user using the given streamType. 177 func (p *Processor) timelineStatus( 178 ctx context.Context, 179 ingest func(context.Context, string, timeline.Timelineable) (bool, error), 180 timelineID string, 181 account *gtsmodel.Account, 182 status *gtsmodel.Status, 183 streamType string, 184 ) (bool, error) { 185 // Make sure the status is timelineable. 186 // This works for both home and list timelines. 187 if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil { 188 err = fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %w", account.ID, err) 189 return false, err 190 } else if !timelineable { 191 // Nothing to do. 192 return false, nil 193 } 194 195 // Ingest status into given timeline using provided function. 196 if inserted, err := ingest(ctx, timelineID, status); err != nil { 197 err = fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err) 198 return false, err 199 } else if !inserted { 200 // Nothing more to do. 201 return false, nil 202 } 203 204 // The status was inserted so stream it to the user. 205 apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account) 206 if err != nil { 207 err = fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err) 208 return true, err 209 } 210 211 if err := p.stream.Update(apiStatus, account, []string{streamType}); err != nil { 212 err = fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err) 213 return true, err 214 } 215 216 return true, nil 217 } 218 219 func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.Status) error { 220 errs := make(gtserror.MultiError, 0, len(status.Mentions)) 221 222 for _, m := range status.Mentions { 223 if err := p.notify( 224 ctx, 225 gtsmodel.NotificationMention, 226 m.TargetAccountID, 227 m.OriginAccountID, 228 m.StatusID, 229 ); err != nil { 230 errs.Append(err) 231 } 232 } 233 234 return errs.Combine() 235 } 236 237 func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { 238 return p.notify( 239 ctx, 240 gtsmodel.NotificationFollowRequest, 241 followRequest.TargetAccountID, 242 followRequest.AccountID, 243 "", 244 ) 245 } 246 247 func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error { 248 // Remove previous follow request notification, if it exists. 249 prevNotif, err := p.state.DB.GetNotification( 250 gtscontext.SetBarebones(ctx), 251 gtsmodel.NotificationFollowRequest, 252 targetAccount.ID, 253 follow.AccountID, 254 "", 255 ) 256 if err != nil && !errors.Is(err, db.ErrNoEntries) { 257 // Proper error while checking. 258 return fmt.Errorf("notifyFollow: db error checking for previous follow request notification: %w", err) 259 } 260 261 if prevNotif != nil { 262 // Previous notification existed, delete. 263 if err := p.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); err != nil { 264 return fmt.Errorf("notifyFollow: db error removing previous follow request notification %s: %w", prevNotif.ID, err) 265 } 266 } 267 268 // Now notify the follow itself. 269 return p.notify( 270 ctx, 271 gtsmodel.NotificationFollow, 272 targetAccount.ID, 273 follow.AccountID, 274 "", 275 ) 276 } 277 278 func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error { 279 if fave.TargetAccountID == fave.AccountID { 280 // Self-fave, nothing to do. 281 return nil 282 } 283 284 return p.notify( 285 ctx, 286 gtsmodel.NotificationFave, 287 fave.TargetAccountID, 288 fave.AccountID, 289 fave.StatusID, 290 ) 291 } 292 293 func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error { 294 if status.BoostOfID == "" { 295 // Not a boost, nothing to do. 296 return nil 297 } 298 299 if status.BoostOfAccountID == status.AccountID { 300 // Self-boost, nothing to do. 301 return nil 302 } 303 304 return p.notify( 305 ctx, 306 gtsmodel.NotificationReblog, 307 status.BoostOfAccountID, 308 status.AccountID, 309 status.ID, 310 ) 311 } 312 313 func (p *Processor) notify( 314 ctx context.Context, 315 notificationType gtsmodel.NotificationType, 316 targetAccountID string, 317 originAccountID string, 318 statusID string, 319 ) error { 320 targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) 321 if err != nil { 322 return fmt.Errorf("notify: error getting target account %s: %w", targetAccountID, err) 323 } 324 325 if !targetAccount.IsLocal() { 326 // Nothing to do. 327 return nil 328 } 329 330 // Make sure a notification doesn't 331 // already exist with these params. 332 if _, err := p.state.DB.GetNotification( 333 ctx, 334 notificationType, 335 targetAccountID, 336 originAccountID, 337 statusID, 338 ); err == nil { 339 // Notification exists, nothing to do. 340 return nil 341 } else if !errors.Is(err, db.ErrNoEntries) { 342 // Real error. 343 return fmt.Errorf("notify: error checking existence of notification: %w", err) 344 } 345 346 // Notification doesn't yet exist, so 347 // we need to create + store one. 348 notif := >smodel.Notification{ 349 ID: id.NewULID(), 350 NotificationType: notificationType, 351 TargetAccountID: targetAccountID, 352 OriginAccountID: originAccountID, 353 StatusID: statusID, 354 } 355 356 if err := p.state.DB.PutNotification(ctx, notif); err != nil { 357 return fmt.Errorf("notify: error putting notification in database: %w", err) 358 } 359 360 // Stream notification to the user. 361 apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif) 362 if err != nil { 363 return fmt.Errorf("notify: error converting notification to api representation: %w", err) 364 } 365 366 if err := p.stream.Notify(apiNotif, targetAccount); err != nil { 367 return fmt.Errorf("notify: error streaming notification to account: %w", err) 368 } 369 370 return nil 371 } 372 373 // wipeStatus contains common logic used to totally delete a status 374 // + all its attachments, notifications, boosts, and timeline entries. 375 func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error { 376 // either delete all attachments for this status, or simply 377 // unattach all attachments for this status, so they'll be 378 // cleaned later by a separate process; reason to unattach rather 379 // than delete is that the poster might want to reattach them 380 // to another status immediately (in case of delete + redraft) 381 if deleteAttachments { 382 // todo: p.state.DB.DeleteAttachmentsForStatus 383 for _, a := range statusToDelete.AttachmentIDs { 384 if err := p.media.Delete(ctx, a); err != nil { 385 return err 386 } 387 } 388 } else { 389 // todo: p.state.DB.UnattachAttachmentsForStatus 390 for _, a := range statusToDelete.AttachmentIDs { 391 if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil { 392 return err 393 } 394 } 395 } 396 397 // delete all mention entries generated by this status 398 // todo: p.state.DB.DeleteMentionsForStatus 399 for _, id := range statusToDelete.MentionIDs { 400 if err := p.state.DB.DeleteMentionByID(ctx, id); err != nil { 401 return err 402 } 403 } 404 405 // delete all notification entries generated by this status 406 if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { 407 return err 408 } 409 410 // delete all bookmarks that point to this status 411 if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { 412 return err 413 } 414 415 // delete all faves of this status 416 if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { 417 return err 418 } 419 420 // delete all boosts for this status + remove them from timelines 421 if boosts, err := p.state.DB.GetStatusReblogs(ctx, statusToDelete); err == nil { 422 for _, b := range boosts { 423 if err := p.deleteStatusFromTimelines(ctx, b.ID); err != nil { 424 return err 425 } 426 if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil { 427 return err 428 } 429 } 430 } 431 432 // delete this status from any and all timelines 433 if err := p.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { 434 return err 435 } 436 437 // delete the status itself 438 return p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID) 439 } 440 441 // deleteStatusFromTimelines completely removes the given status from all timelines. 442 // It will also stream deletion of the status to all open streams. 443 func (p *Processor) deleteStatusFromTimelines(ctx context.Context, statusID string) error { 444 if err := p.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil { 445 return err 446 } 447 448 if err := p.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil { 449 return err 450 } 451 452 return p.stream.Delete(statusID) 453 } 454 455 // invalidateStatusFromTimelines does cache invalidation on the given status by 456 // unpreparing it from all timelines, forcing it to be prepared again (with updated 457 // stats, boost counts, etc) next time it's fetched by the timeline owner. This goes 458 // both for the status itself, and for any boosts of the status. 459 func (p *Processor) invalidateStatusFromTimelines(ctx context.Context, statusID string) { 460 if err := p.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { 461 log. 462 WithContext(ctx). 463 WithField("statusID", statusID). 464 Errorf("error unpreparing status from home timelines: %v", err) 465 } 466 467 if err := p.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { 468 log. 469 WithContext(ctx). 470 WithField("statusID", statusID). 471 Errorf("error unpreparing status from list timelines: %v", err) 472 } 473 } 474 475 /* 476 EMAIL FUNCTIONS 477 */ 478 479 func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) error { 480 instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) 481 if err != nil { 482 return fmt.Errorf("emailReport: error getting instance: %w", err) 483 } 484 485 toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx) 486 if err != nil { 487 if errors.Is(err, db.ErrNoEntries) { 488 // No registered moderator addresses. 489 return nil 490 } 491 return fmt.Errorf("emailReport: error getting instance moderator addresses: %w", err) 492 } 493 494 if report.Account == nil { 495 report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID) 496 if err != nil { 497 return fmt.Errorf("emailReport: error getting report account: %w", err) 498 } 499 } 500 501 if report.TargetAccount == nil { 502 report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID) 503 if err != nil { 504 return fmt.Errorf("emailReport: error getting report target account: %w", err) 505 } 506 } 507 508 reportData := email.NewReportData{ 509 InstanceURL: instance.URI, 510 InstanceName: instance.Title, 511 ReportURL: instance.URI + "/settings/admin/reports/" + report.ID, 512 ReportDomain: report.Account.Domain, 513 ReportTargetDomain: report.TargetAccount.Domain, 514 } 515 516 if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil { 517 return fmt.Errorf("emailReport: error emailing instance moderators: %w", err) 518 } 519 520 return nil 521 } 522 523 func (p *Processor) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error { 524 user, err := p.state.DB.GetUserByAccountID(ctx, report.Account.ID) 525 if err != nil { 526 return fmt.Errorf("emailReportClosed: db error getting user: %w", err) 527 } 528 529 if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" { 530 // Only email users who: 531 // - are confirmed 532 // - are approved 533 // - are not disabled 534 // - have an email address 535 return nil 536 } 537 538 instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) 539 if err != nil { 540 return fmt.Errorf("emailReportClosed: db error getting instance: %w", err) 541 } 542 543 if report.Account == nil { 544 report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID) 545 if err != nil { 546 return fmt.Errorf("emailReportClosed: error getting report account: %w", err) 547 } 548 } 549 550 if report.TargetAccount == nil { 551 report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID) 552 if err != nil { 553 return fmt.Errorf("emailReportClosed: error getting report target account: %w", err) 554 } 555 } 556 557 reportClosedData := email.ReportClosedData{ 558 Username: report.Account.Username, 559 InstanceURL: instance.URI, 560 InstanceName: instance.Title, 561 ReportTargetUsername: report.TargetAccount.Username, 562 ReportTargetDomain: report.TargetAccount.Domain, 563 ActionTakenComment: report.ActionTaken, 564 } 565 566 return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData) 567 }