gtsocial-umbx

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

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, &gtsmodel.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 := &gtsmodel.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 }