gtsocial-umbx

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

public_timeline.go (3938B)


      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 visibility
     19 
     20 import (
     21 	"context"
     22 	"fmt"
     23 	"time"
     24 
     25 	"github.com/superseriousbusiness/gotosocial/internal/cache"
     26 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
     27 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     28 	"github.com/superseriousbusiness/gotosocial/internal/log"
     29 )
     30 
     31 // StatusHomeTimelineable checks if given status should be included on requester's public timeline. Primarily relying on status visibility to requester and the AP visibility setting, and ignoring conversation threads.
     32 func (f *Filter) StatusPublicTimelineable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
     33 	const vtype = cache.VisibilityTypePublic
     34 
     35 	// By default we assume no auth.
     36 	requesterID := noauth
     37 
     38 	if requester != nil {
     39 		// Use provided account ID.
     40 		requesterID = requester.ID
     41 	}
     42 
     43 	visibility, err := f.state.Caches.Visibility.Load("Type.RequesterID.ItemID", func() (*cache.CachedVisibility, error) {
     44 		// Visibility not yet cached, perform timeline visibility lookup.
     45 		visible, err := f.isStatusPublicTimelineable(ctx, requester, status)
     46 		if err != nil {
     47 			return nil, err
     48 		}
     49 
     50 		// Return visibility value.
     51 		return &cache.CachedVisibility{
     52 			ItemID:      status.ID,
     53 			RequesterID: requesterID,
     54 			Type:        vtype,
     55 			Value:       visible,
     56 		}, nil
     57 	}, vtype, requesterID, status.ID)
     58 	if err != nil {
     59 		if err == cache.SentinelError {
     60 			// Filter-out our temporary
     61 			// race-condition error.
     62 			return false, nil
     63 		}
     64 
     65 		return false, err
     66 	}
     67 
     68 	return visibility.Value, nil
     69 }
     70 
     71 func (f *Filter) isStatusPublicTimelineable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
     72 	if status.CreatedAt.After(time.Now().Add(24 * time.Hour)) {
     73 		// Statuses made over 1 day in the future we don't show...
     74 		log.Warnf(ctx, "status >24hrs in the future: %+v", status)
     75 		return false, nil
     76 	}
     77 
     78 	// Don't show boosts on timeline.
     79 	if status.BoostOfID != "" {
     80 		return false, nil
     81 	}
     82 
     83 	// Check whether status is visible to requesting account.
     84 	visible, err := f.StatusVisible(ctx, requester, status)
     85 	if err != nil {
     86 		return false, err
     87 	}
     88 
     89 	if !visible {
     90 		log.Trace(ctx, "status not visible to timeline requester")
     91 		return false, nil
     92 	}
     93 
     94 	for parent := status; parent.InReplyToURI != ""; {
     95 		// Fetch next parent to lookup.
     96 		parentID := parent.InReplyToID
     97 		if parentID == "" {
     98 			log.Warnf(ctx, "status not yet deref'd: %s", parent.InReplyToURI)
     99 			return false, cache.SentinelError
    100 		}
    101 
    102 		// Get the next parent in the chain from DB.
    103 		parent, err = f.state.DB.GetStatusByID(
    104 			gtscontext.SetBarebones(ctx),
    105 			parentID,
    106 		)
    107 		if err != nil {
    108 			return false, fmt.Errorf("isStatusPublicTimelineable: error getting status parent %s: %w", parentID, err)
    109 		}
    110 
    111 		if parent.AccountID != status.AccountID {
    112 			// This is not a single author reply-chain-thread,
    113 			// instead is an actualy conversation. Don't timeline.
    114 			log.Trace(ctx, "ignoring multi-author reply-chain")
    115 			return false, nil
    116 		}
    117 	}
    118 
    119 	// This is either a visible status in a
    120 	// single-author thread, or a visible top
    121 	// level status. Show on public timeline.
    122 	return true, nil
    123 }