summaryrefslogtreecommitdiff
path: root/internal/gtsmodel/filter.go
blob: 1d457d878a544a1d8df5c698f9e8f11480e8a27c (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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// 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 gtsmodel

import (
	"fmt"
	"regexp"
	"strconv"
	"time"

	"code.superseriousbusiness.org/gotosocial/internal/util"
	"codeberg.org/gruf/go-byteutil"
)

// FilterContext represents the
// context in which a Filter applies.
//
// These are used as bit-field masks to determine
// which are enabled in a FilterContexts bit field,
// as well as to signify internally any particular
// context in which a status should be filtered in.
type FilterContext bitFieldType

const (
	// FilterContextNone means no filters should
	// be applied, this is for internal use only.
	FilterContextNone FilterContext = 0

	// FilterContextHome means this status is being
	// filtered as part of a home or list timeline.
	FilterContextHome FilterContext = 1 << 1

	// FilterContextNotifications means this status is
	// being filtered as part of the notifications timeline.
	FilterContextNotifications FilterContext = 1 << 2

	// FilterContextPublic means this status is
	// being filtered as part of a public or tag timeline.
	FilterContextPublic FilterContext = 1 << 3

	// FilterContextThread means this status is
	// being filtered as part of a thread's context.
	FilterContextThread FilterContext = 1 << 4

	// FilterContextAccount means this status is
	// being filtered as part of an account's statuses.
	FilterContextAccount FilterContext = 1 << 5
)

// String returns human-readable form of FilterContext.
func (ctx FilterContext) String() string {
	switch ctx {
	case FilterContextNone:
		return ""
	case FilterContextHome:
		return "home"
	case FilterContextNotifications:
		return "notifications"
	case FilterContextPublic:
		return "public"
	case FilterContextThread:
		return "thread"
	case FilterContextAccount:
		return "account"
	default:
		panic(fmt.Sprintf("invalid filter context: %d", ctx))
	}
}

// FilterContexts stores multiple contexts
// in which a Filter applies as bits in an int.
type FilterContexts bitFieldType

// Applies returns whether receiving FilterContexts applies in FilterContexts.
func (ctxs FilterContexts) Applies(ctx FilterContext) bool {
	return ctxs&FilterContexts(ctx) != 0
}

// Home returns whether FilterContextHome is set.
func (ctxs FilterContexts) Home() bool {
	return ctxs&FilterContexts(FilterContextHome) != 0
}

// SetHome will set the FilterContextHome bit.
func (ctxs *FilterContexts) SetHome() {
	*ctxs |= FilterContexts(FilterContextHome)
}

// UnsetHome will unset the FilterContextHome bit.
func (ctxs *FilterContexts) UnsetHome() {
	*ctxs &= ^FilterContexts(FilterContextHome)
}

// Notifications returns whether FilterContextNotifications is set.
func (ctxs FilterContexts) Notifications() bool {
	return ctxs&FilterContexts(FilterContextNotifications) != 0
}

// SetNotifications will set the FilterContextNotifications bit.
func (ctxs *FilterContexts) SetNotifications() {
	*ctxs |= FilterContexts(FilterContextNotifications)
}

// UnsetNotifications will unset the FilterContextNotifications bit.
func (ctxs *FilterContexts) UnsetNotifications() {
	*ctxs &= ^FilterContexts(FilterContextNotifications)
}

// Public returns whether FilterContextPublic is set.
func (ctxs FilterContexts) Public() bool {
	return ctxs&FilterContexts(FilterContextPublic) != 0
}

// SetPublic will set the FilterContextPublic bit.
func (ctxs *FilterContexts) SetPublic() {
	*ctxs |= FilterContexts(FilterContextPublic)
}

// UnsetPublic will unset the FilterContextPublic bit.
func (ctxs *FilterContexts) UnsetPublic() {
	*ctxs &= ^FilterContexts(FilterContextPublic)
}

// Thread returns whether FilterContextThread is set.
func (ctxs FilterContexts) Thread() bool {
	return ctxs&FilterContexts(FilterContextThread) != 0
}

// SetThread will set the FilterContextThread bit.
func (ctxs *FilterContexts) SetThread() {
	*ctxs |= FilterContexts(FilterContextThread)
}

// UnsetThread will unset the FilterContextThread bit.
func (ctxs *FilterContexts) UnsetThread() {
	*ctxs &= ^FilterContexts(FilterContextThread)
}

// Account returns whether FilterContextAccount is set.
func (ctxs FilterContexts) Account() bool {
	return ctxs&FilterContexts(FilterContextAccount) != 0
}

// SetAccount will set / unset the FilterContextAccount bit.
func (ctxs *FilterContexts) SetAccount() {
	*ctxs |= FilterContexts(FilterContextAccount)
}

// UnsetAccount will unset the FilterContextAccount bit.
func (ctxs *FilterContexts) UnsetAccount() {
	*ctxs &= ^FilterContexts(FilterContextAccount)
}

// String returns a single human-readable form of FilterContexts.
func (ctxs FilterContexts) String() string {
	var buf byteutil.Buffer
	buf.Guarantee(72) // worst-case estimate
	buf.B = append(buf.B, '{')
	buf.B = append(buf.B, "home="...)
	buf.B = strconv.AppendBool(buf.B, ctxs.Home())
	buf.B = append(buf.B, ',')
	buf.B = append(buf.B, "notifications="...)
	buf.B = strconv.AppendBool(buf.B, ctxs.Notifications())
	buf.B = append(buf.B, ',')
	buf.B = append(buf.B, "public="...)
	buf.B = strconv.AppendBool(buf.B, ctxs.Public())
	buf.B = append(buf.B, ',')
	buf.B = append(buf.B, "thread="...)
	buf.B = strconv.AppendBool(buf.B, ctxs.Thread())
	buf.B = append(buf.B, ',')
	buf.B = append(buf.B, "account="...)
	buf.B = strconv.AppendBool(buf.B, ctxs.Account())
	buf.B = append(buf.B, '}')
	return buf.String()
}

// FilterAction represents the action
// to take on a filtered status.
type FilterAction enumType

const (
	// FilterActionNone filters should not exist, except
	// internally, for partially constructed or invalid filters.
	FilterActionNone FilterAction = 0

	// FilterActionWarn means that the
	// status should be shown behind a warning.
	FilterActionWarn FilterAction = 1

	// FilterActionHide means that the status should
	// be removed from timeline results entirely.
	FilterActionHide FilterAction = 2
)

// String returns human-readable form of FilterAction.
func (act FilterAction) String() string {
	switch act {
	case FilterActionNone:
		return ""
	case FilterActionWarn:
		return "warn"
	case FilterActionHide:
		return "hide"
	default:
		panic(fmt.Sprintf("invalid filter action: %d", act))
	}
}

// Filter stores a filter created by a local account.
type Filter struct {
	ID         string           `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                            // id of this item in the database
	ExpiresAt  time.Time        `bun:"type:timestamptz,nullzero"`                                           // Time filter should expire. If null, should not expire.
	AccountID  string           `bun:"type:CHAR(26),notnull,nullzero,unique:filters_account_id_title_uniq"` // ID of the local account that created the filter.
	Title      string           `bun:",nullzero,notnull,unique:filters_account_id_title_uniq"`              // The name of the filter.
	Action     FilterAction     `bun:",nullzero,notnull,default:0"`                                         // The action to take.
	Keywords   []*FilterKeyword `bun:"-"`                                                                   // Keywords for this filter.
	KeywordIDs []string         `bun:"keywords,array"`                                                      //
	Statuses   []*FilterStatus  `bun:"-"`                                                                   // Statuses for this filter.
	StatusIDs  []string         `bun:"statuses,array"`                                                      //
	Contexts   FilterContexts   `bun:",nullzero,notnull,default:0"`                                         // Which contexts does this filter apply in?
}

// KeywordsPopulated returns whether keywords
// are populated according to current KeywordIDs.
func (f *Filter) KeywordsPopulated() bool {
	if len(f.KeywordIDs) != len(f.Keywords) {
		// this is the quickest indicator.
		return false
	}
	for i, id := range f.KeywordIDs {
		if f.Keywords[i].ID != id {
			return false
		}
	}
	return true
}

// StatusesPopulated returns whether statuses
// are populated according to current StatusIDs.
func (f *Filter) StatusesPopulated() bool {
	if len(f.StatusIDs) != len(f.Statuses) {
		// this is the quickest indicator.
		return false
	}
	for i, id := range f.StatusIDs {
		if f.Statuses[i].ID != id {
			return false
		}
	}
	return true
}

// Expired returns whether the filter has expired at a given time.
// Filters without an expiration timestamp never expire.
func (f *Filter) Expired(now time.Time) bool {
	return !f.ExpiresAt.IsZero() && !f.ExpiresAt.After(now)
}

// FilterKeyword stores a single keyword to filter statuses against.
type FilterKeyword struct {
	ID        string         `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                     // id of this item in the database
	FilterID  string         `bun:"type:CHAR(26),notnull,nullzero,unique:filter_keywords_filter_id_keyword_uniq"` // ID of the filter that this keyword belongs to.
	Keyword   string         `bun:",nullzero,notnull,unique:filter_keywords_filter_id_keyword_uniq"`              // The keyword or phrase to filter against.
	WholeWord *bool          `bun:",nullzero,notnull,default:false"`                                              // Should the filter consider word boundaries?
	Regexp    *regexp.Regexp `bun:"-"`                                                                            // pre-prepared regular expression
}

// Compile will compile this FilterKeyword as a prepared regular expression.
func (k *FilterKeyword) Compile() (err error) {
	var (
		wordBreakStart string
		wordBreakEnd   string
	)

	if util.PtrOrZero(k.WholeWord) {
		// Either word boundary or
		// whitespace or start of line.
		wordBreakStart = `(?:\b|\s|^)`

		// Either word boundary or
		// whitespace or end of line.
		wordBreakEnd = `(?:\b|\s|$)`
	}

	// Compile keyword filter regexp.
	quoted := regexp.QuoteMeta(k.Keyword)
	k.Regexp, err = regexp.Compile(`(?i)` + wordBreakStart + quoted + wordBreakEnd)
	return // caller is expected to wrap this error
}

// FilterStatus stores a single status to filter.
type FilterStatus struct {
	ID       string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                       // id of this item in the database
	FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the filter that this keyword belongs to.
	StatusID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the status to filter.
}