summaryrefslogtreecommitdiff
path: root/internal/gtsmodel/filter.go
diff options
context:
space:
mode:
authorLibravatar kim <grufwub@gmail.com>2025-06-24 17:24:34 +0200
committerLibravatar tobi <kipvandenbos@noreply.codeberg.org>2025-06-24 17:24:34 +0200
commit996da6e0291b158093d917ca76933584f464d668 (patch)
tree38c12b20f76076f08ef5c8b8715ba3d8629fa0fb /internal/gtsmodel/filter.go
parent[bugfix] update the default configuration to not set a db type or address, to... (diff)
downloadgotosocial-996da6e0291b158093d917ca76933584f464d668.tar.xz
[performance] filter model and database table improvements (#4277)
- removes unnecessary fields / columns (created_at, updated_at) - replaces filter.context_* columns with singular filter.contexts bit field which should save both struct memory and database space - replaces filter.action string with integer enum type which should save both struct memory and database space - adds links from filter to filter_* tables with Filter{}.KeywordIDs and Filter{}.StatusIDs fields (this also means we now have those ID slices cached, which reduces some lookups) - removes account_id fields from filter_* tables, since there's a more direct connection between filter and filter_* tables, and filter.account_id already exists - refactors a bunch of the filter processor logic to save on code repetition, factor in the above changes, fix a few bugs with missed error returns and bring it more in-line with some of our newer code Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4277 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/gtsmodel/filter.go')
-rw-r--r--internal/gtsmodel/filter.go278
1 files changed, 241 insertions, 37 deletions
diff --git a/internal/gtsmodel/filter.go b/internal/gtsmodel/filter.go
index 36ebc8391..1d457d878 100644
--- a/internal/gtsmodel/filter.go
+++ b/internal/gtsmodel/filter.go
@@ -18,28 +18,251 @@
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
- CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
- 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"` // The action to take.
- Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter.
- Statuses []*FilterStatus `bun:"-"` // Statuses for this filter.
- ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
- ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications.
- ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
- ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread.
- ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile.
+ 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.
@@ -51,11 +274,7 @@ func (f *Filter) Expired(now time.Time) bool {
// 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
- CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
- AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_keywords_filter_id_keyword_uniq"` // ID of the filter that this keyword belongs to.
- Filter *Filter `bun:"-"` // Filter corresponding to FilterID
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
@@ -72,6 +291,7 @@ func (k *FilterKeyword) Compile() (err error) {
// Either word boundary or
// whitespace or start of line.
wordBreakStart = `(?:\b|\s|^)`
+
// Either word boundary or
// whitespace or end of line.
wordBreakEnd = `(?:\b|\s|$)`
@@ -85,23 +305,7 @@ func (k *FilterKeyword) Compile() (err 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
- CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
- AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
- 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.
- Filter *Filter `bun:"-"` // Filter corresponding to FilterID
- StatusID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the status to filter.
+ 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.
}
-
-// FilterAction represents the action to take on a filtered status.
-type FilterAction string
-
-const (
- // FilterActionNone filters should not exist, except internally, for partially constructed or invalid filters.
- FilterActionNone FilterAction = ""
- // FilterActionWarn means that the status should be shown behind a warning.
- FilterActionWarn FilterAction = "warn"
- // FilterActionHide means that the status should be removed from timeline results entirely.
- FilterActionHide FilterAction = "hide"
-)