summaryrefslogtreecommitdiff
path: root/internal/visibility/statusvisible.go
blob: 002db1119e7819fad07fb8b37abea47cda44d5f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/*
   GoToSocial
   Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package visibility

import (
	"context"
	"fmt"

	"codeberg.org/gruf/go-kv"
	"github.com/superseriousbusiness/gotosocial/internal/db"
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
	"github.com/superseriousbusiness/gotosocial/internal/log"
)

func (f *filter) StatusVisible(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (bool, error) {
	l := log.WithFields(kv.Fields{{"statusID", targetStatus.ID}}...)

	// Fetch any relevant accounts for the target status
	const getBoosted = true
	relevantAccounts, err := f.relevantAccounts(ctx, targetStatus, getBoosted)
	if err != nil {
		l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err)
		return false, fmt.Errorf("StatusVisible: error pulling relevant accounts for status %s: %s", targetStatus.ID, err)
	}

	// Check we have determined a target account
	targetAccount := relevantAccounts.Account
	if targetAccount == nil {
		l.Trace("target account is not set")
		return false, nil
	}

	// Check for domain blocks among relevant accounts
	domainBlocked, err := f.domainBlockedRelevant(ctx, relevantAccounts)
	if err != nil {
		l.Debugf("error checking domain block: %s", err)
		return false, fmt.Errorf("error checking domain block: %s", err)
	} else if domainBlocked {
		return false, nil
	}

	// if target account is suspended then don't show the status
	if !targetAccount.SuspendedAt.IsZero() {
		l.Trace("target account suspended at is not zero")
		return false, nil
	}

	// if the target user doesn't exist (anymore) then the status also shouldn't be visible
	// note: we only do this for local users
	if targetAccount.Domain == "" {
		targetUser, err := f.db.GetUserByAccountID(ctx, targetAccount.ID)
		if err != nil {
			l.Debug("target user could not be selected")
			if err == db.ErrNoEntries {
				return false, nil
			}
			return false, fmt.Errorf("StatusVisible: db error selecting user for local target account %s: %s", targetAccount.ID, err)
		}

		// if target user is disabled, not yet approved, or not confirmed then don't show the status
		// (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!)
		if *targetUser.Disabled || !*targetUser.Approved || targetUser.ConfirmedAt.IsZero() {
			l.Trace("target user is disabled, not approved, or not confirmed")
			return false, nil
		}
	}

	// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed.
	// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't.
	if requestingAccount == nil {
		if targetStatus.Visibility == gtsmodel.VisibilityPublic {
			return true, nil
		}
		l.Trace("requesting account is nil but the target status isn't public")
		return false, nil
	}

	// if the requesting user doesn't exist (anymore) then the status also shouldn't be visible
	// note: we only do this for local users
	if requestingAccount.Domain == "" {
		requestingUser, err := f.db.GetUserByAccountID(ctx, requestingAccount.ID)
		if err != nil {
			// if the requesting account is local but doesn't have a corresponding user in the db this is a problem
			l.Debug("requesting user could not be selected")
			if err == db.ErrNoEntries {
				return false, nil
			}
			return false, fmt.Errorf("StatusVisible: db error selecting user for local requesting account %s: %s", requestingAccount.ID, err)
		}
		// okay, user exists, so make sure it has full privileges/is confirmed/approved
		if *requestingUser.Disabled || !*requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
			l.Trace("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
			return false, nil
		}
	}

	// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten
	// this far (ie., been authed) in the first place: this is just for safety.
	if !requestingAccount.SuspendedAt.IsZero() {
		l.Trace("requesting account is suspended")
		return false, nil
	}

	// if the target status belongs to the requesting account, they should always be able to view it at this point
	if targetStatus.AccountID == requestingAccount.ID {
		return true, nil
	}

	// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou
	// First check if a block exists directly between the target account (which authored the status) and the requesting account.
	if blocked, err := f.db.IsBlocked(ctx, targetAccount.ID, requestingAccount.ID, true); err != nil {
		l.Debugf("something went wrong figuring out if the accounts have a block: %s", err)
		return false, err
	} else if blocked {
		// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please
		l.Trace("a block exists between requesting account and target account")
		return false, nil
	}

	// If not in reply to the requesting account, check if inReplyToAccount is blocked
	if relevantAccounts.InReplyToAccount != nil && relevantAccounts.InReplyToAccount.ID != requestingAccount.ID {
		if blocked, err := f.db.IsBlocked(ctx, relevantAccounts.InReplyToAccount.ID, requestingAccount.ID, true); err != nil {
			return false, err
		} else if blocked {
			l.Trace("a block exists between requesting account and reply to account")
			return false, nil
		}
	}

	// status boosts accounts id
	if relevantAccounts.BoostedAccount != nil {
		if blocked, err := f.db.IsBlocked(ctx, relevantAccounts.BoostedAccount.ID, requestingAccount.ID, true); err != nil {
			return false, err
		} else if blocked {
			l.Trace("a block exists between requesting account and boosted account")
			return false, nil
		}
	}

	// status boosts a reply to account id
	if relevantAccounts.BoostedInReplyToAccount != nil {
		if blocked, err := f.db.IsBlocked(ctx, relevantAccounts.BoostedInReplyToAccount.ID, requestingAccount.ID, true); err != nil {
			return false, err
		} else if blocked {
			l.Trace("a block exists between requesting account and boosted reply to account")
			return false, nil
		}
	}

	// boost mentions accounts
	for _, a := range relevantAccounts.BoostedMentionedAccounts {
		if a == nil {
			continue
		}
		if blocked, err := f.db.IsBlocked(ctx, a.ID, requestingAccount.ID, true); err != nil {
			return false, err
		} else if blocked {
			l.Trace("a block exists between requesting account and a boosted mentioned account")
			return false, nil
		}
	}

	// Iterate mentions to check for blocks or requester mentions
	isMentioned, blockAmongMentions := false, false
	for _, a := range relevantAccounts.MentionedAccounts {
		if a == nil {
			continue
		}

		if blocked, err := f.db.IsBlocked(ctx, a.ID, requestingAccount.ID, true); err != nil {
			return false, err
		} else if blocked {
			blockAmongMentions = true
			break
		}

		if a.ID == requestingAccount.ID {
			isMentioned = true
		}
	}

	if blockAmongMentions {
		l.Trace("a block exists between requesting account and a mentioned account")
		return false, nil
	} else if isMentioned {
		// Requester mentioned, should always be visible
		return true, nil
	}

	// at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status
	// that means it's now just a matter of checking the visibility settings of the status itself
	switch targetStatus.Visibility {
	case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked:
		// no problem here
	case gtsmodel.VisibilityFollowersOnly:
		// Followers-only post, check for a one-way follow to target
		follows, err := f.db.IsFollowing(ctx, requestingAccount, targetAccount)
		if err != nil {
			return false, err
		}
		if !follows {
			l.Trace("requested status is followers only but requesting account is not a follower")
			return false, nil
		}
	case gtsmodel.VisibilityMutualsOnly:
		// Mutuals-only post, check for a mutual follow
		mutuals, err := f.db.IsMutualFollowing(ctx, requestingAccount, targetAccount)
		if err != nil {
			return false, err
		}
		if !mutuals {
			l.Trace("requested status is mutuals only but accounts aren't mufos")
			return false, nil
		}
	case gtsmodel.VisibilityDirect:
		l.Trace("requesting account requests a direct status it's not mentioned in")
		return false, nil // it's not mentioned -_-
	}

	// If we reached here, all is okay
	return true, nil
}

func (f *filter) StatusesVisible(ctx context.Context, statuses []*gtsmodel.Status, requestingAccount *gtsmodel.Account) ([]*gtsmodel.Status, error) {
	filtered := []*gtsmodel.Status{}
	for _, s := range statuses {
		visible, err := f.StatusVisible(ctx, s, requestingAccount)
		if err != nil {
			return nil, err
		}
		if visible {
			filtered = append(filtered, s)
		}
	}
	return filtered, nil
}