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 }