summaryrefslogtreecommitdiff
path: root/internal/processing/fromcommon.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/fromcommon.go')
-rw-r--r--internal/processing/fromcommon.go587
1 files changed, 0 insertions, 587 deletions
diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go
deleted file mode 100644
index 07895b6ba..000000000
--- a/internal/processing/fromcommon.go
+++ /dev/null
@@ -1,587 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-package processing
-
-import (
- "context"
- "errors"
-
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
-)
-
-// timelineAndNotifyStatus processes the given new status and inserts it into
-// the HOME and LIST timelines of accounts that follow the status author.
-//
-// It will also handle notifications for any mentions attached to the account, and
-// also notifications for any local accounts that want to know when this account posts.
-func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
- // Ensure status fully populated; including account, mentions, etc.
- if err := p.state.DB.PopulateStatus(ctx, status); err != nil {
- return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
- }
-
- // Get local followers of the account that posted the status.
- follows, err := p.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
- if err != nil {
- return gtserror.Newf("error getting local followers for account id %s: %w", status.AccountID, err)
- }
-
- // If the poster is also local, add a fake entry for them
- // so they can see their own status in their timeline.
- if status.Account.IsLocal() {
- follows = append(follows, &gtsmodel.Follow{
- AccountID: status.AccountID,
- Account: status.Account,
- Notify: func() *bool { b := false; return &b }(), // Account shouldn't notify itself.
- ShowReblogs: func() *bool { b := true; return &b }(), // Account should show own reblogs.
- })
- }
-
- // Timeline the status for each local follower of this account.
- // This will also handle notifying any followers with notify
- // set to true on their follow.
- if err := p.timelineAndNotifyStatusForFollowers(ctx, status, follows); err != nil {
- return gtserror.Newf("error timelining status %s for followers: %w", status.ID, err)
- }
-
- // Notify each local account that's mentioned by this status.
- if err := p.notifyStatusMentions(ctx, status); err != nil {
- return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
- }
-
- return nil
-}
-
-func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, status *gtsmodel.Status, follows []*gtsmodel.Follow) error {
- var (
- errs = gtserror.NewMultiError(len(follows))
- boost = status.BoostOfID != ""
- reply = status.InReplyToURI != ""
- )
-
- for _, follow := range follows {
- if sr := follow.ShowReblogs; boost && (sr == nil || !*sr) {
- // This is a boost, but this follower
- // doesn't want to see those from this
- // account, so just skip everything.
- continue
- }
-
- // Add status to each list that this follow
- // is included in, and stream it if applicable.
- listEntries, err := p.state.DB.GetListEntriesForFollowID(
- // We only need the list IDs.
- gtscontext.SetBarebones(ctx),
- follow.ID,
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- errs.Appendf("error list timelining status: %w", err)
- continue
- }
-
- for _, listEntry := range listEntries {
- if _, err := p.timelineStatus(
- ctx,
- p.state.Timelines.List.IngestOne,
- listEntry.ListID, // list timelines are keyed by list ID
- follow.Account,
- status,
- stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
- ); err != nil {
- errs.Appendf("error list timelining status: %w", err)
- continue
- }
- }
-
- // Add status to home timeline for this
- // follower, and stream it if applicable.
- if timelined, err := p.timelineStatus(
- ctx,
- p.state.Timelines.Home.IngestOne,
- follow.AccountID, // home timelines are keyed by account ID
- follow.Account,
- status,
- stream.TimelineHome,
- ); err != nil {
- errs.Appendf("error home timelining status: %w", err)
- continue
- } else if !timelined {
- // Status wasn't added to home tomeline,
- // so we shouldn't notify it either.
- continue
- }
-
- if n := follow.Notify; n == nil || !*n {
- // This follower doesn't have notifications
- // set for this account's new posts, so bail.
- continue
- }
-
- if boost || reply {
- // Don't notify for boosts or replies.
- continue
- }
-
- // If we reach here, we know:
- //
- // - This follower wants to be notified when this account posts.
- // - This is a top-level post (not a reply).
- // - This is not a boost of another post.
- // - The post is visible in this follower's home timeline.
- //
- // That means we can officially notify this one.
- if err := p.notify(
- ctx,
- gtsmodel.NotificationStatus,
- follow.AccountID,
- status.AccountID,
- status.ID,
- ); err != nil {
- errs.Appendf("error notifying account %s about new status: %w", follow.AccountID, err)
- }
- }
-
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
-}
-
-// timelineStatus uses the provided ingest function to put the given
-// status in a timeline with the given ID, if it's timelineable.
-//
-// If the status was inserted into the timeline, true will be returned
-// + it will also be streamed to the user using the given streamType.
-func (p *Processor) timelineStatus(
- ctx context.Context,
- ingest func(context.Context, string, timeline.Timelineable) (bool, error),
- timelineID string,
- account *gtsmodel.Account,
- status *gtsmodel.Status,
- streamType string,
-) (bool, error) {
- // Make sure the status is timelineable.
- // This works for both home and list timelines.
- if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil {
- err = gtserror.Newf("error getting timelineability for status for timeline with id %s: %w", account.ID, err)
- return false, err
- } else if !timelineable {
- // Nothing to do.
- return false, nil
- }
-
- // Ingest status into given timeline using provided function.
- if inserted, err := ingest(ctx, timelineID, status); err != nil {
- err = gtserror.Newf("error ingesting status %s: %w", status.ID, err)
- return false, err
- } else if !inserted {
- // Nothing more to do.
- return false, nil
- }
-
- // The status was inserted so stream it to the user.
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account)
- if err != nil {
- err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
- return true, err
- }
-
- if err := p.stream.Update(apiStatus, account, []string{streamType}); err != nil {
- err = gtserror.Newf("error streaming update for status %s: %w", status.ID, err)
- return true, err
- }
-
- return true, nil
-}
-
-func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.Status) error {
- errs := gtserror.NewMultiError(len(status.Mentions))
-
- for _, m := range status.Mentions {
- if err := p.notify(
- ctx,
- gtsmodel.NotificationMention,
- m.TargetAccountID,
- m.OriginAccountID,
- m.StatusID,
- ); err != nil {
- errs.Append(err)
- }
- }
-
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
-}
-
-func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
- return p.notify(
- ctx,
- gtsmodel.NotificationFollowRequest,
- followRequest.TargetAccountID,
- followRequest.AccountID,
- "",
- )
-}
-
-func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error {
- // Remove previous follow request notification, if it exists.
- prevNotif, err := p.state.DB.GetNotification(
- gtscontext.SetBarebones(ctx),
- gtsmodel.NotificationFollowRequest,
- targetAccount.ID,
- follow.AccountID,
- "",
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- // Proper error while checking.
- return gtserror.Newf("db error checking for previous follow request notification: %w", err)
- }
-
- if prevNotif != nil {
- // Previous notification existed, delete.
- if err := p.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); err != nil {
- return gtserror.Newf("db error removing previous follow request notification %s: %w", prevNotif.ID, err)
- }
- }
-
- // Now notify the follow itself.
- return p.notify(
- ctx,
- gtsmodel.NotificationFollow,
- targetAccount.ID,
- follow.AccountID,
- "",
- )
-}
-
-func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
- if fave.TargetAccountID == fave.AccountID {
- // Self-fave, nothing to do.
- return nil
- }
-
- return p.notify(
- ctx,
- gtsmodel.NotificationFave,
- fave.TargetAccountID,
- fave.AccountID,
- fave.StatusID,
- )
-}
-
-func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error {
- if status.BoostOfID == "" {
- // Not a boost, nothing to do.
- return nil
- }
-
- if status.BoostOfAccountID == status.AccountID {
- // Self-boost, nothing to do.
- return nil
- }
-
- return p.notify(
- ctx,
- gtsmodel.NotificationReblog,
- status.BoostOfAccountID,
- status.AccountID,
- status.ID,
- )
-}
-
-func (p *Processor) notify(
- ctx context.Context,
- notificationType gtsmodel.NotificationType,
- targetAccountID string,
- originAccountID string,
- statusID string,
-) error {
- targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
- if err != nil {
- return gtserror.Newf("error getting target account %s: %w", targetAccountID, err)
- }
-
- if !targetAccount.IsLocal() {
- // Nothing to do.
- return nil
- }
-
- // Make sure a notification doesn't
- // already exist with these params.
- if _, err := p.state.DB.GetNotification(
- ctx,
- notificationType,
- targetAccountID,
- originAccountID,
- statusID,
- ); err == nil {
- // Notification exists, nothing to do.
- return nil
- } else if !errors.Is(err, db.ErrNoEntries) {
- // Real error.
- return gtserror.Newf("error checking existence of notification: %w", err)
- }
-
- // Notification doesn't yet exist, so
- // we need to create + store one.
- notif := &gtsmodel.Notification{
- ID: id.NewULID(),
- NotificationType: notificationType,
- TargetAccountID: targetAccountID,
- OriginAccountID: originAccountID,
- StatusID: statusID,
- }
-
- if err := p.state.DB.PutNotification(ctx, notif); err != nil {
- return gtserror.Newf("error putting notification in database: %w", err)
- }
-
- // Stream notification to the user.
- apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif)
- if err != nil {
- return gtserror.Newf("error converting notification to api representation: %w", err)
- }
-
- if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
- return gtserror.Newf("error streaming notification to account: %w", err)
- }
-
- return nil
-}
-
-// wipeStatus contains common logic used to totally delete a status
-// + all its attachments, notifications, boosts, and timeline entries.
-func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error {
- var errs gtserror.MultiError
-
- // either delete all attachments for this status, or simply
- // unattach all attachments for this status, so they'll be
- // cleaned later by a separate process; reason to unattach rather
- // than delete is that the poster might want to reattach them
- // to another status immediately (in case of delete + redraft)
- if deleteAttachments {
- // todo: p.state.DB.DeleteAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if err := p.media.Delete(ctx, a); err != nil {
- errs.Appendf("error deleting media: %w", err)
- }
- }
- } else {
- // todo: p.state.DB.UnattachAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil {
- errs.Appendf("error unattaching media: %w", err)
- }
- }
- }
-
- // delete all mention entries generated by this status
- // todo: p.state.DB.DeleteMentionsForStatus
- for _, id := range statusToDelete.MentionIDs {
- if err := p.state.DB.DeleteMentionByID(ctx, id); err != nil {
- errs.Appendf("error deleting status mention: %w", err)
- }
- }
-
- // delete all notification entries generated by this status
- if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status notifications: %w", err)
- }
-
- // delete all bookmarks that point to this status
- if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status bookmarks: %w", err)
- }
-
- // delete all faves of this status
- if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status faves: %w", err)
- }
-
- // delete all boosts for this status + remove them from timelines
- boosts, err := p.state.DB.GetStatusBoosts(
- // we MUST set a barebones context here,
- // as depending on where it came from the
- // original BoostOf may already be gone.
- gtscontext.SetBarebones(ctx),
- statusToDelete.ID)
- if err != nil {
- errs.Appendf("error fetching status boosts: %w", err)
- }
- for _, b := range boosts {
- if err := p.deleteStatusFromTimelines(ctx, b.ID); err != nil {
- errs.Appendf("error deleting boost from timelines: %w", err)
- }
- if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
- errs.Appendf("error deleting boost: %w", err)
- }
- }
-
- // delete this status from any and all timelines
- if err := p.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status from timelines: %w", err)
- }
-
- // finally, delete the status itself
- if err := p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status: %w", err)
- }
-
- return errs.Combine()
-}
-
-// deleteStatusFromTimelines completely removes the given status from all timelines.
-// It will also stream deletion of the status to all open streams.
-func (p *Processor) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
- if err := p.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
-
- if err := p.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
-
- return p.stream.Delete(statusID)
-}
-
-// invalidateStatusFromTimelines does cache invalidation on the given status by
-// unpreparing it from all timelines, forcing it to be prepared again (with updated
-// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
-// both for the status itself, and for any boosts of the status.
-func (p *Processor) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
- if err := p.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
- log.
- WithContext(ctx).
- WithField("statusID", statusID).
- Errorf("error unpreparing status from home timelines: %v", err)
- }
-
- if err := p.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
- log.
- WithContext(ctx).
- WithField("statusID", statusID).
- Errorf("error unpreparing status from list timelines: %v", err)
- }
-}
-
-/*
- EMAIL FUNCTIONS
-*/
-
-func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) error {
- instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
- if err != nil {
- return gtserror.Newf("error getting instance: %w", err)
- }
-
- toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // No registered moderator addresses.
- return nil
- }
- return gtserror.Newf("error getting instance moderator addresses: %w", err)
- }
-
- if report.Account == nil {
- report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
- if err != nil {
- return gtserror.Newf("error getting report account: %w", err)
- }
- }
-
- if report.TargetAccount == nil {
- report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return gtserror.Newf("error getting report target account: %w", err)
- }
- }
-
- reportData := email.NewReportData{
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ReportURL: instance.URI + "/settings/admin/reports/" + report.ID,
- ReportDomain: report.Account.Domain,
- ReportTargetDomain: report.TargetAccount.Domain,
- }
-
- if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
- return gtserror.Newf("error emailing instance moderators: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error {
- user, err := p.state.DB.GetUserByAccountID(ctx, report.Account.ID)
- if err != nil {
- return gtserror.Newf("db error getting user: %w", err)
- }
-
- if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" {
- // Only email users who:
- // - are confirmed
- // - are approved
- // - are not disabled
- // - have an email address
- return nil
- }
-
- instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
- if err != nil {
- return gtserror.Newf("db error getting instance: %w", err)
- }
-
- if report.Account == nil {
- report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
- if err != nil {
- return gtserror.Newf("error getting report account: %w", err)
- }
- }
-
- if report.TargetAccount == nil {
- report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return gtserror.Newf("error getting report target account: %w", err)
- }
- }
-
- reportClosedData := email.ReportClosedData{
- Username: report.Account.Username,
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ReportTargetUsername: report.TargetAccount.Username,
- ReportTargetDomain: report.TargetAccount.Domain,
- ActionTakenComment: report.ActionTaken,
- }
-
- return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData)
-}