gtsocial-umbx

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

home_timeline.go (7337B)


      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 owner's home timeline. Primarily relying on status visibility to owner and the AP visibility setting, but also taking into account thread replies etc.
     32 func (f *Filter) StatusHomeTimelineable(ctx context.Context, owner *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
     33 	const vtype = cache.VisibilityTypeHome
     34 
     35 	// By default we assume no auth.
     36 	requesterID := noauth
     37 
     38 	if owner != nil {
     39 		// Use provided account ID.
     40 		requesterID = owner.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.isStatusHomeTimelineable(ctx, owner, 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) isStatusHomeTimelineable(ctx context.Context, owner *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 	// Check whether status is visible to timeline owner.
     79 	visible, err := f.StatusVisible(ctx, owner, status)
     80 	if err != nil {
     81 		return false, err
     82 	}
     83 
     84 	if !visible {
     85 		log.Trace(ctx, "status not visible to timeline owner")
     86 		return false, nil
     87 	}
     88 
     89 	if status.AccountID == owner.ID {
     90 		// Author can always see their status.
     91 		return true, nil
     92 	}
     93 
     94 	if status.MentionsAccount(owner.ID) {
     95 		// Can always see when you are mentioned.
     96 		return true, nil
     97 	}
     98 
     99 	var (
    100 		next      *gtsmodel.Status
    101 		oneAuthor = true // Assume one author until proven otherwise.
    102 		included  bool
    103 		converstn bool
    104 	)
    105 
    106 	for next = status; next.InReplyToURI != ""; {
    107 		// Fetch next parent to lookup.
    108 		parentID := next.InReplyToID
    109 		if parentID == "" {
    110 			log.Warnf(ctx, "status not yet deref'd: %s", next.InReplyToURI)
    111 			return false, cache.SentinelError
    112 		}
    113 
    114 		// Get the next parent in the chain from DB.
    115 		next, err = f.state.DB.GetStatusByID(
    116 			gtscontext.SetBarebones(ctx),
    117 			parentID,
    118 		)
    119 		if err != nil {
    120 			return false, fmt.Errorf("isStatusHomeTimelineable: error getting status parent %s: %w", parentID, err)
    121 		}
    122 
    123 		// Populate account mention objects before account mention checks.
    124 		next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs)
    125 		if err != nil {
    126 			return false, fmt.Errorf("isStatusHomeTimelineable: error populating status parent %s mentions: %w", parentID, err)
    127 		}
    128 
    129 		if (next.AccountID == owner.ID) ||
    130 			next.MentionsAccount(owner.ID) {
    131 			// Owner is in / mentioned in
    132 			// this status thread. They can
    133 			// see all future visible statuses.
    134 			included = true
    135 			break
    136 		}
    137 
    138 		// Check whether this should be a visible conversation, i.e.
    139 		// is it between accounts on owner timeline that they follow?
    140 		converstn, err = f.isVisibleConversation(ctx, owner, next)
    141 		if err != nil {
    142 			return false, fmt.Errorf("isStatusHomeTimelineable: error checking conversation visibility: %w", err)
    143 		}
    144 
    145 		if converstn {
    146 			// Owner is relevant to this conversation,
    147 			// i.e. between follows / mutuals they know.
    148 			break
    149 		}
    150 
    151 		if oneAuthor {
    152 			// Check if this continues to be a single-author thread.
    153 			oneAuthor = (next.AccountID == status.AccountID)
    154 		}
    155 	}
    156 
    157 	if next != status && !oneAuthor && !included && !converstn {
    158 		log.Trace(ctx, "ignoring visible reply in conversation irrelevant to owner")
    159 		return false, nil
    160 	}
    161 
    162 	// At this point status is either a top-level status, a reply in a single
    163 	// author thread (e.g. "this is my weird-ass take and here is why 1/10 🧵"),
    164 	// a status thread *including* the owner, or a conversation thread between
    165 	// accounts the timeline owner follows.
    166 
    167 	if status.Visibility == gtsmodel.VisibilityFollowersOnly ||
    168 		status.Visibility == gtsmodel.VisibilityMutualsOnly {
    169 		// Followers/mutuals only post that already passed the status
    170 		// visibility check, (i.e. we follow / mutuals with author).
    171 		return true, nil
    172 	}
    173 
    174 	// Ensure owner follows author of public/unlocked status.
    175 	follow, err := f.state.DB.IsFollowing(ctx,
    176 		owner.ID,
    177 		status.AccountID,
    178 	)
    179 	if err != nil {
    180 		return false, fmt.Errorf("isStatusHomeTimelineable: error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
    181 	}
    182 
    183 	if !follow {
    184 		log.Trace(ctx, "ignoring visible status from unfollowed author")
    185 		return false, nil
    186 	}
    187 
    188 	return true, nil
    189 }
    190 
    191 func (f *Filter) isVisibleConversation(ctx context.Context, owner *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
    192 	// Check if status is visible to the timeline owner.
    193 	visible, err := f.StatusVisible(ctx, owner, status)
    194 	if err != nil {
    195 		return false, err
    196 	}
    197 
    198 	if !visible {
    199 		// Invisible to
    200 		// timeline owner.
    201 		return false, nil
    202 	}
    203 
    204 	if status.Visibility == gtsmodel.VisibilityUnlocked ||
    205 		status.Visibility == gtsmodel.VisibilityPublic {
    206 		// NOTE: there is no need to check in the case of
    207 		// direct / follow-only / mutual-only visibility statuses
    208 		// as the above visibility check already handles this.
    209 
    210 		// Check if owner follows the status author.
    211 		followAuthor, err := f.state.DB.IsFollowing(ctx,
    212 			owner.ID,
    213 			status.AccountID,
    214 		)
    215 		if err != nil {
    216 			return false, fmt.Errorf("error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
    217 		}
    218 
    219 		if !followAuthor {
    220 			// Not a visible status
    221 			// in conversation thread.
    222 			return false, nil
    223 		}
    224 	}
    225 
    226 	for _, mention := range status.Mentions {
    227 		// Check if timeline owner follows target.
    228 		follow, err := f.state.DB.IsFollowing(ctx,
    229 			owner.ID,
    230 			mention.TargetAccountID,
    231 		)
    232 		if err != nil {
    233 			return false, fmt.Errorf("error checking mention follow %s->%s: %w", owner.ID, mention.TargetAccountID, err)
    234 		}
    235 
    236 		if follow {
    237 			// Confirmed conversation.
    238 			return true, nil
    239 		}
    240 	}
    241 
    242 	return false, nil
    243 }