status.go (7181B)
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 24 "github.com/superseriousbusiness/gotosocial/internal/cache" 25 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 26 "github.com/superseriousbusiness/gotosocial/internal/log" 27 ) 28 29 // StatusesVisible calls StatusVisible for each status in the statuses slice, and returns a slice of only statuses which are visible to the requester. 30 func (f *Filter) StatusesVisible(ctx context.Context, requester *gtsmodel.Account, statuses []*gtsmodel.Status) ([]*gtsmodel.Status, error) { 31 // Preallocate slice of maximum possible length. 32 filtered := make([]*gtsmodel.Status, 0, len(statuses)) 33 34 for _, status := range statuses { 35 // Check whether status is visible to requester. 36 visible, err := f.StatusVisible(ctx, requester, status) 37 if err != nil { 38 return nil, err 39 } 40 41 if visible { 42 // Add filtered status to ret slice. 43 filtered = append(filtered, status) 44 } 45 } 46 47 return filtered, nil 48 } 49 50 // StatusVisible will check if given status is visible to requester, accounting for requester with no auth (i.e is nil), suspensions, disabled local users, account blocks and status privacy. 51 func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { 52 const vtype = cache.VisibilityTypeStatus 53 54 // By default we assume no auth. 55 requesterID := noauth 56 57 if requester != nil { 58 // Use provided account ID. 59 requesterID = requester.ID 60 } 61 62 visibility, err := f.state.Caches.Visibility.Load("Type.RequesterID.ItemID", func() (*cache.CachedVisibility, error) { 63 // Visibility not yet cached, perform visibility lookup. 64 visible, err := f.isStatusVisible(ctx, requester, status) 65 if err != nil { 66 return nil, err 67 } 68 69 // Return visibility value. 70 return &cache.CachedVisibility{ 71 ItemID: status.ID, 72 RequesterID: requesterID, 73 Type: vtype, 74 Value: visible, 75 }, nil 76 }, vtype, requesterID, status.ID) 77 if err != nil { 78 return false, err 79 } 80 81 return visibility.Value, nil 82 } 83 84 // isStatusVisible will check if status is visible to requester. It is the "meat" of the logic to Filter{}.StatusVisible() which is called within cache loader callback. 85 func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { 86 // Ensure that status is fully populated for further processing. 87 if err := f.state.DB.PopulateStatus(ctx, status); err != nil { 88 return false, fmt.Errorf("isStatusVisible: error populating status %s: %w", status.ID, err) 89 } 90 91 // Check whether status accounts are visible to the requester. 92 visible, err := f.areStatusAccountsVisible(ctx, requester, status) 93 if err != nil { 94 return false, fmt.Errorf("isStatusVisible: error checking status %s account visibility: %w", status.ID, err) 95 } else if !visible { 96 return false, nil 97 } 98 99 if status.Visibility == gtsmodel.VisibilityPublic { 100 // This status will be visible to all. 101 return true, nil 102 } 103 104 if requester == nil { 105 // This request is WITHOUT auth, and status is NOT public. 106 log.Trace(ctx, "unauthorized request to non-public status") 107 return false, nil 108 } 109 110 if status.Visibility == gtsmodel.VisibilityUnlocked { 111 // This status is visible to all auth'd accounts. 112 return true, nil 113 } 114 115 if requester.ID == status.AccountID { 116 // Author can always see their own status. 117 return true, nil 118 } 119 120 if status.MentionsAccount(requester.ID) { 121 // Status mentions the requesting account. 122 return true, nil 123 } 124 125 if status.BoostOf != nil { 126 if !status.BoostOf.MentionsPopulated() { 127 // Boosted status needs its mentions populating, fetch these from database. 128 status.BoostOf.Mentions, err = f.state.DB.GetMentions(ctx, status.BoostOf.MentionIDs) 129 if err != nil { 130 return false, fmt.Errorf("isStatusVisible: error populating boosted status %s mentions: %w", status.BoostOfID, err) 131 } 132 } 133 134 if status.BoostOf.MentionsAccount(requester.ID) { 135 // Boosted status mentions the requesting account. 136 return true, nil 137 } 138 } 139 140 switch status.Visibility { 141 case gtsmodel.VisibilityFollowersOnly: 142 // Check requester follows status author. 143 follows, err := f.state.DB.IsFollowing(ctx, 144 requester.ID, 145 status.AccountID, 146 ) 147 if err != nil { 148 return false, fmt.Errorf("isStatusVisible: error checking follow %s->%s: %w", requester.ID, status.AccountID, err) 149 } 150 151 if !follows { 152 log.Trace(ctx, "follow-only status not visible to requester") 153 return false, nil 154 } 155 156 return true, nil 157 158 case gtsmodel.VisibilityMutualsOnly: 159 // Check mutual following between requester and author. 160 mutuals, err := f.state.DB.IsMutualFollowing(ctx, 161 requester.ID, 162 status.AccountID, 163 ) 164 if err != nil { 165 return false, fmt.Errorf("isStatusVisible: error checking mutual follow %s<->%s: %w", requester.ID, status.AccountID, err) 166 } 167 168 if !mutuals { 169 log.Trace(ctx, "mutual-only status not visible to requester") 170 return false, nil 171 } 172 173 return true, nil 174 175 case gtsmodel.VisibilityDirect: 176 log.Trace(ctx, "direct status not visible to requester") 177 return false, nil 178 179 default: 180 log.Warnf(ctx, "unexpected status visibility %s for %s", status.Visibility, status.URI) 181 return false, nil 182 } 183 } 184 185 // areStatusAccountsVisible calls Filter{}.AccountVisible() on status author and the status boost-of (if set) author, returning visibility of status (and boost-of) to requester. 186 func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { 187 // Check whether status author's account is visible to requester. 188 visible, err := f.AccountVisible(ctx, requester, status.Account) 189 if err != nil { 190 return false, fmt.Errorf("error checking status author visibility: %w", err) 191 } 192 193 if !visible { 194 log.Trace(ctx, "status author not visible to requester") 195 return false, nil 196 } 197 198 if status.BoostOfID != "" { 199 // This is a boosted status. 200 201 if status.AccountID == status.BoostOfAccountID { 202 // Some clout-chaser boosted their own status, tch. 203 return true, nil 204 } 205 206 // Check whether boosted status author's account is visible to requester. 207 visible, err := f.AccountVisible(ctx, requester, status.BoostOfAccount) 208 if err != nil { 209 return false, fmt.Errorf("error checking boosted author visibility: %w", err) 210 } 211 212 if !visible { 213 log.Trace(ctx, "boosted status author not visible to requester") 214 return false, nil 215 } 216 } 217 218 return true, nil 219 }