diff --git a/internal/api/client/filters/v1/filterdelete_test.go b/internal/api/client/filters/v1/filterdelete_test.go
index 314da05bd..ee1ab581e 100644
--- a/internal/api/client/filters/v1/filterdelete_test.go
+++ b/internal/api/client/filters/v1/filterdelete_test.go
@@ -104,7 +104,7 @@ func (suite *FiltersTestSuite) TestDeleteFilter() {
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
- err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -113,7 +113,7 @@ func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestDeleteNonexistentFilter() {
id := "not_even_a_real_ULID"
- err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v1/filterget_test.go b/internal/api/client/filters/v1/filterget_test.go
index 93090c880..fcb6fd6c5 100644
--- a/internal/api/client/filters/v1/filterget_test.go
+++ b/internal/api/client/filters/v1/filterget_test.go
@@ -108,7 +108,7 @@ func (suite *FiltersTestSuite) TestGetFilter() {
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
- _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -117,7 +117,7 @@ func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestGetNonexistentFilter() {
id := "not_even_a_real_ULID"
- _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v1/filterput_test.go b/internal/api/client/filters/v1/filterput_test.go
index 41804ad44..c6edbaf0b 100644
--- a/internal/api/client/filters/v1/filterput_test.go
+++ b/internal/api/client/filters/v1/filterput_test.go
@@ -261,7 +261,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
phrase := "GNU/Linux"
context := []string{"home"}
- _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -271,7 +271,7 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
id := "not_even_a_real_ULID"
phrase := "GNU/Linux"
context := []string{"home"}
- _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v1/filtersget_test.go b/internal/api/client/filters/v1/filtersget_test.go
index bf8ed8a0b..c2d84cd43 100644
--- a/internal/api/client/filters/v1/filtersget_test.go
+++ b/internal/api/client/filters/v1/filtersget_test.go
@@ -87,10 +87,17 @@ func (suite *FiltersTestSuite) getFilters(
func (suite *FiltersTestSuite) TestGetFilters() {
// v1 filters map to individual filter keywords.
+ wrappingFilterIDs := make(map[string]struct{}, len(suite.testFilters))
expectedFilterIDs := make([]string, 0, len(suite.testFilterKeywords))
expectedFilterKeywords := make([]string, 0, len(suite.testFilterKeywords))
+ testAccountID := suite.testAccounts["local_account_1"].ID
+ for _, filter := range suite.testFilters {
+ if filter.AccountID == testAccountID {
+ wrappingFilterIDs[filter.ID] = struct{}{}
+ }
+ }
for _, filterKeyword := range suite.testFilterKeywords {
- if filterKeyword.AccountID == suite.testAccounts["local_account_1"].ID {
+ if _, ok := wrappingFilterIDs[filterKeyword.FilterID]; ok {
expectedFilterIDs = append(expectedFilterIDs, filterKeyword.ID)
expectedFilterKeywords = append(expectedFilterKeywords, filterKeyword.Keyword)
}
diff --git a/internal/api/client/filters/v2/filterdelete_test.go b/internal/api/client/filters/v2/filterdelete_test.go
index b2d6ae6de..3c997bfef 100644
--- a/internal/api/client/filters/v2/filterdelete_test.go
+++ b/internal/api/client/filters/v2/filterdelete_test.go
@@ -104,7 +104,7 @@ func (suite *FiltersTestSuite) TestDeleteFilter() {
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
id := suite.testFilters["local_account_2_filter_1"].ID
- err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -113,7 +113,7 @@ func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestDeleteNonexistentFilter() {
id := "not_even_a_real_ULID"
- err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterget_test.go b/internal/api/client/filters/v2/filterget_test.go
index 48d90cae2..c58ed46db 100644
--- a/internal/api/client/filters/v2/filterget_test.go
+++ b/internal/api/client/filters/v2/filterget_test.go
@@ -106,7 +106,7 @@ func (suite *FiltersTestSuite) TestGetFilter() {
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
id := suite.testFilters["local_account_2_filter_1"].ID
- _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -115,7 +115,7 @@ func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestGetNonexistentFilter() {
id := "not_even_a_real_ULID"
- _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterkeyworddelete_test.go b/internal/api/client/filters/v2/filterkeyworddelete_test.go
index 1b9325c6a..308e5e011 100644
--- a/internal/api/client/filters/v2/filterkeyworddelete_test.go
+++ b/internal/api/client/filters/v2/filterkeyworddelete_test.go
@@ -104,7 +104,7 @@ func (suite *FiltersTestSuite) TestDeleteFilterKeyword() {
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterKeyword() {
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
- err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -113,7 +113,7 @@ func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterKeyword() {
func (suite *FiltersTestSuite) TestDeleteNonexistentFilterKeyword() {
id := "not_even_a_real_ULID"
- err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterkeywordget_test.go b/internal/api/client/filters/v2/filterkeywordget_test.go
index 25167700d..f04090d90 100644
--- a/internal/api/client/filters/v2/filterkeywordget_test.go
+++ b/internal/api/client/filters/v2/filterkeywordget_test.go
@@ -106,7 +106,7 @@ func (suite *FiltersTestSuite) TestGetFilterKeyword() {
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterKeyword() {
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
- _, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -115,7 +115,7 @@ func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterKeyword() {
func (suite *FiltersTestSuite) TestGetNonexistentFilterKeyword() {
id := "not_even_a_real_ULID"
- _, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterkeywordpost_test.go b/internal/api/client/filters/v2/filterkeywordpost_test.go
index ce7087b0b..8f37a3030 100644
--- a/internal/api/client/filters/v2/filterkeywordpost_test.go
+++ b/internal/api/client/filters/v2/filterkeywordpost_test.go
@@ -189,7 +189,7 @@ func (suite *FiltersTestSuite) TestPostFilterKeywordKeywordConflict() {
func (suite *FiltersTestSuite) TestPostFilterKeywordAnotherAccountsFilter() {
filterID := suite.testFilters["local_account_2_filter_1"].ID
keyword := "fnords"
- _, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -198,7 +198,7 @@ func (suite *FiltersTestSuite) TestPostFilterKeywordAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestPostFilterKeywordNonexistentFilter() {
filterID := "not_even_a_real_ULID"
keyword := "fnords"
- _, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterkeywordput_test.go b/internal/api/client/filters/v2/filterkeywordput_test.go
index e98dd899d..e07def26e 100644
--- a/internal/api/client/filters/v2/filterkeywordput_test.go
+++ b/internal/api/client/filters/v2/filterkeywordput_test.go
@@ -189,7 +189,7 @@ func (suite *FiltersTestSuite) TestPutFilterKeywordKeywordConflict() {
func (suite *FiltersTestSuite) TestPutFilterKeywordAnotherAccountsFilterKeyword() {
filterKeywordID := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
keyword := "fnord"
- _, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -198,7 +198,7 @@ func (suite *FiltersTestSuite) TestPutFilterKeywordAnotherAccountsFilterKeyword(
func (suite *FiltersTestSuite) TestPutFilterKeywordNonexistentFilterKeyword() {
filterKeywordID := "not_even_a_real_ULID"
keyword := "fnord"
- _, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter keyword not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterput_test.go b/internal/api/client/filters/v2/filterput_test.go
index 4fd1d26ee..c8e8cb9c6 100644
--- a/internal/api/client/filters/v2/filterput_test.go
+++ b/internal/api/client/filters/v2/filterput_test.go
@@ -116,7 +116,7 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context
return nil, err
}
- errs := gtserror.NewMultiError(2)
+ var errs gtserror.MultiError
// check code + body
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
@@ -324,7 +324,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
id := suite.testFilters["local_account_1_filter_1"].ID
title := suite.testFilters["local_account_1_filter_2"].Title
- _, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`, nil)
+ _, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: duplicate title"}`, nil)
if err != nil {
suite.FailNow(err.Error())
}
@@ -334,7 +334,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
id := suite.testFilters["local_account_2_filter_1"].ID
title := "GNU/Linux"
context := []string{"home"}
- _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
+ _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`, nil)
if err != nil {
suite.FailNow(err.Error())
}
@@ -344,7 +344,7 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
id := "not_even_a_real_ULID"
phrase := "GNU/Linux"
context := []string{"home"}
- _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
+ _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`, nil)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filtersget_test.go b/internal/api/client/filters/v2/filtersget_test.go
index 76802d83a..a410bdc90 100644
--- a/internal/api/client/filters/v2/filtersget_test.go
+++ b/internal/api/client/filters/v2/filtersget_test.go
@@ -86,10 +86,8 @@ func (suite *FiltersTestSuite) getFilters(
}
func (suite *FiltersTestSuite) TestGetFilters() {
- // Set of filter IDs for the test user.
- expectedFilterIDs := []string{}
// Map of filter IDs to filter keyword and status IDs.
- expectedFilters := map[string]struct {
+ expectedFilters := map[string]*struct {
keywordIDs []string
statusIDs []string
}{}
@@ -98,28 +96,26 @@ func (suite *FiltersTestSuite) TestGetFilters() {
accountID := suite.testAccounts["local_account_1"].ID
for _, filter := range suite.testFilters {
if filter.AccountID == accountID {
- expectedFilterIDs = append(expectedFilterIDs, filter.ID)
- expectedFilters[filter.ID] = struct {
+ expectedFilters[filter.ID] = &struct {
keywordIDs []string
statusIDs []string
}{}
}
}
for _, filterKeyword := range suite.testFilterKeywords {
- if filterKeyword.AccountID == accountID {
- expectedIDsForFilter := expectedFilters[filterKeyword.FilterID]
- expectedIDsForFilter.keywordIDs = append(expectedIDsForFilter.keywordIDs, filterKeyword.ID)
- expectedFilters[filterKeyword.FilterID] = expectedIDsForFilter
+ expected, ok := expectedFilters[filterKeyword.FilterID]
+ if !ok {
+ continue
}
+ expected.keywordIDs = append(expected.keywordIDs, filterKeyword.ID)
}
for _, filterStatus := range suite.testFilterStatuses {
- if filterStatus.AccountID == accountID {
- expectedIDsForFilter := expectedFilters[filterStatus.FilterID]
- expectedIDsForFilter.statusIDs = append(expectedIDsForFilter.statusIDs, filterStatus.ID)
- expectedFilters[filterStatus.FilterID] = expectedIDsForFilter
+ expected, ok := expectedFilters[filterStatus.FilterID]
+ if !ok {
+ continue
}
+ expected.statusIDs = append(expected.statusIDs, filterStatus.ID)
}
- suite.NotEmpty(expectedFilterIDs)
suite.NotEmpty(expectedFilters)
// Fetch all filters for the logged-in account.
@@ -132,23 +128,19 @@ func (suite *FiltersTestSuite) TestGetFilters() {
// Check that we got the right ones.
suite.Len(filters, len(expectedFilters))
- actualFilterIDs := []string{}
for _, filter := range filters {
- actualFilterIDs = append(actualFilterIDs, filter.ID)
-
expectedIDsForFilter := expectedFilters[filter.ID]
- actualFilterKeywordIDs := []string{}
+ var actualFilterKeywordIDs []string
for _, filterKeyword := range filter.Keywords {
actualFilterKeywordIDs = append(actualFilterKeywordIDs, filterKeyword.ID)
}
suite.ElementsMatch(actualFilterKeywordIDs, expectedIDsForFilter.keywordIDs)
- actualFilterStatusIDs := []string{}
+ var actualFilterStatusIDs []string
for _, filterStatus := range filter.Statuses {
actualFilterStatusIDs = append(actualFilterStatusIDs, filterStatus.ID)
}
suite.ElementsMatch(actualFilterStatusIDs, expectedIDsForFilter.statusIDs)
}
- suite.ElementsMatch(expectedFilterIDs, actualFilterIDs)
}
diff --git a/internal/api/client/filters/v2/filterstatusdelete_test.go b/internal/api/client/filters/v2/filterstatusdelete_test.go
index 017b2739b..bb918d76a 100644
--- a/internal/api/client/filters/v2/filterstatusdelete_test.go
+++ b/internal/api/client/filters/v2/filterstatusdelete_test.go
@@ -101,7 +101,7 @@ func (suite *FiltersTestSuite) TestDeleteFilterStatus() {
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterStatus() {
id := suite.testFilterStatuses["local_account_2_filter_1_status_1"].ID
- err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -110,7 +110,7 @@ func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterStatus() {
func (suite *FiltersTestSuite) TestDeleteNonexistentFilterStatus() {
id := "not_even_a_real_ULID"
- err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found: filter status not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterstatusget_test.go b/internal/api/client/filters/v2/filterstatusget_test.go
index d7c27eed2..f5ea0a1ba 100644
--- a/internal/api/client/filters/v2/filterstatusget_test.go
+++ b/internal/api/client/filters/v2/filterstatusget_test.go
@@ -104,7 +104,7 @@ func (suite *FiltersTestSuite) TestGetFilterStatus() {
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterStatus() {
id := suite.testFilterStatuses["local_account_2_filter_1_status_1"].ID
- _, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -113,7 +113,7 @@ func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterStatus() {
func (suite *FiltersTestSuite) TestGetNonexistentFilterStatus() {
id := "not_even_a_real_ULID"
- _, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found: filter status not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/client/filters/v2/filterstatuspost_test.go b/internal/api/client/filters/v2/filterstatuspost_test.go
index d6a960372..acfd73e02 100644
--- a/internal/api/client/filters/v2/filterstatuspost_test.go
+++ b/internal/api/client/filters/v2/filterstatuspost_test.go
@@ -173,7 +173,7 @@ func (suite *FiltersTestSuite) TestPostFilterStatusStatusIDConflict() {
func (suite *FiltersTestSuite) TestPostFilterStatusAnotherAccountsFilter() {
filterID := suite.testFilters["local_account_2_filter_1"].ID
statusID := suite.testStatuses["admin_account_status_1"].ID
- _, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -182,7 +182,7 @@ func (suite *FiltersTestSuite) TestPostFilterStatusAnotherAccountsFilter() {
func (suite *FiltersTestSuite) TestPostFilterStatusNonexistentFilter() {
filterID := "not_even_a_real_ULID"
statusID := suite.testStatuses["admin_account_status_1"].ID
- _, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found: filter not found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 54777441f..d3d2d5f2b 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -86,6 +86,7 @@ func (c *Caches) Init() {
c.initDomainPermissionExclude()
c.initEmoji()
c.initEmojiCategory()
+ c.initFilterIDs()
c.initFilter()
c.initFilterKeyword()
c.initFilterStatus()
diff --git a/internal/cache/db.go b/internal/cache/db.go
index 5592ca493..6b482d5f8 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -82,6 +82,10 @@ type DBCaches struct {
// Filter provides access to the gtsmodel Filter database cache.
Filter StructCache[*gtsmodel.Filter]
+ // FilterIDs provides access to the filter IDs database cache.
+ // This cache is keyed as: {accountID} -> []{filterIDs}.
+ FilterIDs SliceCache[string]
+
// FilterKeyword provides access to the gtsmodel FilterKeyword database cache.
FilterKeyword StructCache[*gtsmodel.FilterKeyword]
@@ -691,12 +695,23 @@ func (c *Caches) initFilter() {
{Fields: "ID"},
{Fields: "AccountID", Multiple: true},
},
- MaxSize: cap,
- IgnoreErr: ignoreErrors,
- Copy: copyF,
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ Copy: copyF,
+ Invalidate: c.OnInvalidateFilter,
})
}
+func (c *Caches) initFilterIDs() {
+ cap := calculateSliceCacheMax(
+ config.GetCacheFilterIDsMemRatio(),
+ )
+
+ log.Infof(nil, "cache size = %d", cap)
+
+ c.DB.FilterIDs.Init(0, cap)
+}
+
func (c *Caches) initFilterKeyword() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
@@ -710,11 +725,6 @@ func (c *Caches) initFilterKeyword() {
filterKeyword2 := new(gtsmodel.FilterKeyword)
*filterKeyword2 = *filterKeyword1
- // Don't include ptr fields that
- // will be populated separately.
- // See internal/db/bundb/filter.go.
- filterKeyword2.Filter = nil
-
// We specifically DO NOT unset
// the regexp field here, as any
// regexp.Regexp instance is safe
@@ -726,12 +736,12 @@ func (c *Caches) initFilterKeyword() {
c.DB.FilterKeyword.Init(structr.CacheConfig[*gtsmodel.FilterKeyword]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
- {Fields: "AccountID", Multiple: true},
{Fields: "FilterID", Multiple: true},
},
- MaxSize: cap,
- IgnoreErr: ignoreErrors,
- Copy: copyF,
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ Copy: copyF,
+ Invalidate: c.OnInvalidateFilterKeyword,
})
}
@@ -747,24 +757,18 @@ func (c *Caches) initFilterStatus() {
copyF := func(filterStatus1 *gtsmodel.FilterStatus) *gtsmodel.FilterStatus {
filterStatus2 := new(gtsmodel.FilterStatus)
*filterStatus2 = *filterStatus1
-
- // Don't include ptr fields that
- // will be populated separately.
- // See internal/db/bundb/filter.go.
- filterStatus2.Filter = nil
-
return filterStatus2
}
c.DB.FilterStatus.Init(structr.CacheConfig[*gtsmodel.FilterStatus]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
- {Fields: "AccountID", Multiple: true},
{Fields: "FilterID", Multiple: true},
},
- MaxSize: cap,
- IgnoreErr: ignoreErrors,
- Copy: copyF,
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ Copy: copyF,
+ Invalidate: c.OnInvalidateFilterStatus,
})
}
diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go
index 88b7415ae..4941b2540 100644
--- a/internal/cache/invalidate.go
+++ b/internal/cache/invalidate.go
@@ -112,6 +112,29 @@ func (c *Caches) OnInvalidateEmojiCategory(category *gtsmodel.EmojiCategory) {
c.DB.Emoji.Invalidate("CategoryID", category.ID)
}
+func (c *Caches) OnInvalidateFilter(filter *gtsmodel.Filter) {
+ // Invalidate list of filters for account.
+ c.DB.FilterIDs.Invalidate(filter.AccountID)
+
+ // Invalidate all associated keywords and statuses.
+ c.DB.FilterKeyword.InvalidateIDs("ID", filter.KeywordIDs)
+ c.DB.FilterStatus.InvalidateIDs("ID", filter.StatusIDs)
+
+ // Invalidate account's timelines (in case local).
+ c.Timelines.Home.Unprepare(filter.AccountID)
+ c.Timelines.List.Unprepare(filter.AccountID)
+}
+
+func (c *Caches) OnInvalidateFilterKeyword(filterKeyword *gtsmodel.FilterKeyword) {
+ // Invalidate filter that keyword associated with.
+ c.DB.Filter.Invalidate("ID", filterKeyword.FilterID)
+}
+
+func (c *Caches) OnInvalidateFilterStatus(filterStatus *gtsmodel.FilterStatus) {
+ // Invalidate filter that status associated with.
+ c.DB.Filter.Invalidate("ID", filterStatus.FilterID)
+}
+
func (c *Caches) OnInvalidateFollow(follow *gtsmodel.Follow) {
// Invalidate follow request with this same ID.
c.DB.FollowRequest.Invalidate("ID", follow.ID)
diff --git a/internal/cache/size.go b/internal/cache/size.go
index ef9259f88..8a6c9e9ad 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -19,6 +19,7 @@ package cache
import (
"crypto/rsa"
+ "regexp"
"strings"
"time"
"unsafe"
@@ -348,8 +349,6 @@ func sizeofEmojiCategory() uintptr {
func sizeofFilter() uintptr {
return uintptr(size.Of(>smodel.Filter{
ID: exampleID,
- CreatedAt: exampleTime,
- UpdatedAt: exampleTime,
ExpiresAt: exampleTime,
AccountID: exampleID,
Title: exampleTextSmall,
@@ -359,21 +358,18 @@ func sizeofFilter() uintptr {
func sizeofFilterKeyword() uintptr {
return uintptr(size.Of(>smodel.FilterKeyword{
- ID: exampleID,
- CreatedAt: exampleTime,
- UpdatedAt: exampleTime,
- FilterID: exampleID,
- Keyword: exampleTextSmall,
+ ID: exampleID,
+ FilterID: exampleID,
+ Keyword: exampleTextSmall,
+ Regexp: regexp.MustCompile("^match (this)? .*"),
}))
}
func sizeofFilterStatus() uintptr {
return uintptr(size.Of(>smodel.FilterStatus{
- ID: exampleID,
- CreatedAt: exampleTime,
- UpdatedAt: exampleTime,
- FilterID: exampleID,
- StatusID: exampleID,
+ ID: exampleID,
+ FilterID: exampleID,
+ StatusID: exampleID,
}))
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 4bb7db801..528000478 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -226,6 +226,7 @@ type CacheConfiguration struct {
EmojiMemRatio float64 `name:"emoji-mem-ratio"`
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
FilterMemRatio float64 `name:"filter-mem-ratio"`
+ FilterIDsMemRatio float64 `name:"filter-ids-mem-ratio"`
FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"`
FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"`
FollowMemRatio float64 `name:"follow-mem-ratio"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 234f0f8d0..82de65bb7 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -193,6 +193,7 @@ var Defaults = Configuration{
EmojiMemRatio: 3,
EmojiCategoryMemRatio: 0.1,
FilterMemRatio: 0.5,
+ FilterIDsMemRatio: 2,
FilterKeywordMemRatio: 0.5,
FilterStatusMemRatio: 0.5,
FollowMemRatio: 2,
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index e8ed3b0d6..7dfe1db23 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -170,6 +170,7 @@ const (
CacheEmojiMemRatioFlag = "cache-emoji-mem-ratio"
CacheEmojiCategoryMemRatioFlag = "cache-emoji-category-mem-ratio"
CacheFilterMemRatioFlag = "cache-filter-mem-ratio"
+ CacheFilterIDsMemRatioFlag = "cache-filter-ids-mem-ratio"
CacheFilterKeywordMemRatioFlag = "cache-filter-keyword-mem-ratio"
CacheFilterStatusMemRatioFlag = "cache-filter-status-mem-ratio"
CacheFollowMemRatioFlag = "cache-follow-mem-ratio"
@@ -362,6 +363,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.Float64("cache-emoji-mem-ratio", cfg.Cache.EmojiMemRatio, "")
flags.Float64("cache-emoji-category-mem-ratio", cfg.Cache.EmojiCategoryMemRatio, "")
flags.Float64("cache-filter-mem-ratio", cfg.Cache.FilterMemRatio, "")
+ flags.Float64("cache-filter-ids-mem-ratio", cfg.Cache.FilterIDsMemRatio, "")
flags.Float64("cache-filter-keyword-mem-ratio", cfg.Cache.FilterKeywordMemRatio, "")
flags.Float64("cache-filter-status-mem-ratio", cfg.Cache.FilterStatusMemRatio, "")
flags.Float64("cache-follow-mem-ratio", cfg.Cache.FollowMemRatio, "")
@@ -406,7 +408,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
}
func (cfg *Configuration) MarshalMap() map[string]any {
- cfgmap := make(map[string]any, 189)
+ cfgmap := make(map[string]any, 190)
cfgmap["log-level"] = cfg.LogLevel
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
cfgmap["log-db-queries"] = cfg.LogDbQueries
@@ -548,6 +550,7 @@ func (cfg *Configuration) MarshalMap() map[string]any {
cfgmap["cache-emoji-mem-ratio"] = cfg.Cache.EmojiMemRatio
cfgmap["cache-emoji-category-mem-ratio"] = cfg.Cache.EmojiCategoryMemRatio
cfgmap["cache-filter-mem-ratio"] = cfg.Cache.FilterMemRatio
+ cfgmap["cache-filter-ids-mem-ratio"] = cfg.Cache.FilterIDsMemRatio
cfgmap["cache-filter-keyword-mem-ratio"] = cfg.Cache.FilterKeywordMemRatio
cfgmap["cache-filter-status-mem-ratio"] = cfg.Cache.FilterStatusMemRatio
cfgmap["cache-follow-mem-ratio"] = cfg.Cache.FollowMemRatio
@@ -1767,6 +1770,14 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
}
}
+ if ival, ok := cfgmap["cache-filter-ids-mem-ratio"]; ok {
+ var err error
+ cfg.Cache.FilterIDsMemRatio, err = cast.ToFloat64E(ival)
+ if err != nil {
+ return fmt.Errorf("error casting %#v -> float64 for 'cache-filter-ids-mem-ratio': %w", ival, err)
+ }
+ }
+
if ival, ok := cfgmap["cache-filter-keyword-mem-ratio"]; ok {
var err error
cfg.Cache.FilterKeywordMemRatio, err = cast.ToFloat64E(ival)
@@ -5278,6 +5289,28 @@ func GetCacheFilterMemRatio() float64 { return global.GetCacheFilterMemRatio() }
// SetCacheFilterMemRatio safely sets the value for global configuration 'Cache.FilterMemRatio' field
func SetCacheFilterMemRatio(v float64) { global.SetCacheFilterMemRatio(v) }
+// GetCacheFilterIDsMemRatio safely fetches the Configuration value for state's 'Cache.FilterIDsMemRatio' field
+func (st *ConfigState) GetCacheFilterIDsMemRatio() (v float64) {
+ st.mutex.RLock()
+ v = st.config.Cache.FilterIDsMemRatio
+ st.mutex.RUnlock()
+ return
+}
+
+// SetCacheFilterIDsMemRatio safely sets the Configuration value for state's 'Cache.FilterIDsMemRatio' field
+func (st *ConfigState) SetCacheFilterIDsMemRatio(v float64) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.FilterIDsMemRatio = v
+ st.reloadToViper()
+}
+
+// GetCacheFilterIDsMemRatio safely fetches the value for global configuration 'Cache.FilterIDsMemRatio' field
+func GetCacheFilterIDsMemRatio() float64 { return global.GetCacheFilterIDsMemRatio() }
+
+// SetCacheFilterIDsMemRatio safely sets the value for global configuration 'Cache.FilterIDsMemRatio' field
+func SetCacheFilterIDsMemRatio(v float64) { global.SetCacheFilterIDsMemRatio(v) }
+
// GetCacheFilterKeywordMemRatio safely fetches the Configuration value for state's 'Cache.FilterKeywordMemRatio' field
func (st *ConfigState) GetCacheFilterKeywordMemRatio() (v float64) {
st.mutex.RLock()
@@ -6359,6 +6392,7 @@ func (st *ConfigState) GetTotalOfMemRatios() (total float64) {
total += st.config.Cache.EmojiMemRatio
total += st.config.Cache.EmojiCategoryMemRatio
total += st.config.Cache.FilterMemRatio
+ total += st.config.Cache.FilterIDsMemRatio
total += st.config.Cache.FilterKeywordMemRatio
total += st.config.Cache.FilterStatusMemRatio
total += st.config.Cache.FollowMemRatio
@@ -6910,6 +6944,17 @@ func flattenConfigMap(cfgmap map[string]any) {
}
}
+ for _, key := range [][]string{
+ {"cache", "filter-ids-mem-ratio"},
+ } {
+ ival, ok := mapGet(cfgmap, key...)
+ if ok {
+ cfgmap["cache-filter-ids-mem-ratio"] = ival
+ nestedKeys[key[0]] = struct{}{}
+ break
+ }
+ }
+
for _, key := range [][]string{
{"cache", "filter-keyword-mem-ratio"},
} {
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index ba705dd33..6545414a7 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -35,6 +35,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/gotosocial/internal/observability"
@@ -118,11 +119,17 @@ func doMigration(ctx context.Context, db *bun.DB) error {
log.Infof(ctx, "MIGRATED DATABASE TO %s", group)
if db.Dialect().Name() == dialect.SQLite {
- log.Info(ctx,
- "running ANALYZE to update table and index statistics; this will take somewhere between "+
- "1-10 minutes, or maybe longer depending on your hardware and database size, please be patient",
- )
- _, err := db.ExecContext(ctx, "ANALYZE")
+ // Perform a final WAL checkpoint after a migration on SQLite.
+ if strings.EqualFold(config.GetDbSqliteJournalMode(), "WAL") {
+ _, err := db.ExecContext(ctx, "PRAGMA wal_checkpoint(RESTART);")
+ if err != nil {
+ return gtserror.Newf("error performing wal_checkpoint: %w", err)
+ }
+ }
+
+ log.Info(ctx, "running ANALYZE to update table and index statistics; this will take somewhere between "+
+ "1-10 minutes, or maybe longer depending on your hardware and database size, please be patient")
+ _, err := db.ExecContext(ctx, "ANALYZE;")
if err != nil {
log.Warnf(ctx, "ANALYZE failed, query planner may make poor life choices: %s", err)
}
diff --git a/internal/db/bundb/filter.go b/internal/db/bundb/filter.go
index 24208b1f3..dbc560a12 100644
--- a/internal/db/bundb/filter.go
+++ b/internal/db/bundb/filter.go
@@ -21,8 +21,8 @@ import (
"context"
"errors"
"slices"
- "time"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -64,24 +64,14 @@ func (f *filterDB) GetFilterByID(ctx context.Context, id string) (*gtsmodel.Filt
return filter, nil
}
-func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) {
- // Fetch IDs of all filters owned by this account.
- var filterIDs []string
- if err := f.db.
- NewSelect().
- Model((*gtsmodel.Filter)(nil)).
- Column("id").
- Where("? = ?", bun.Ident("account_id"), accountID).
- Scan(ctx, &filterIDs); err != nil {
- return nil, err
- }
- if len(filterIDs) == 0 {
+func (f *filterDB) GetFiltersByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Filter, error) {
+ if len(ids) == 0 {
return nil, nil
}
// Get each filter by ID from the cache or DB.
filters, err := f.state.Caches.DB.Filter.LoadIDs("ID",
- filterIDs,
+ ids,
func(uncached []string) ([]*gtsmodel.Filter, error) {
filters := make([]*gtsmodel.Filter, 0, len(uncached))
if err := f.db.
@@ -99,14 +89,15 @@ func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string)
}
// Put the filter structs in the same order as the filter IDs.
- xslices.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
+ xslices.OrderBy(filters, ids, func(filter *gtsmodel.Filter) string { return filter.ID })
if gtscontext.Barebones(ctx) {
return filters, nil
}
+ var errs gtserror.MultiError
+
// Populate the filters. Remove any that we can't populate from the return slice.
- errs := gtserror.NewMultiError(len(filters))
filters = slices.DeleteFunc(filters, func(filter *gtsmodel.Filter) bool {
if err := f.populateFilter(ctx, filter); err != nil {
errs.Appendf("error populating filter %s: %w", filter.ID, err)
@@ -118,235 +109,115 @@ func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string)
return filters, errs.Combine()
}
+func (f *filterDB) GetFilterIDsByAccountID(ctx context.Context, accountID string) ([]string, error) {
+ return f.state.Caches.DB.FilterIDs.Load(accountID, func() ([]string, error) {
+ var filterIDs []string
+
+ if err := f.db.
+ NewSelect().
+ Model((*gtsmodel.Filter)(nil)).
+ Column("id").
+ Where("? = ?", bun.Ident("account_id"), accountID).
+ Scan(ctx, &filterIDs); err != nil {
+ return nil, err
+ }
+
+ return filterIDs, nil
+ })
+}
+
+func (f *filterDB) GetFiltersByAccountID(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) {
+ filterIDs, err := f.GetFilterIDsByAccountID(ctx, accountID)
+ if err != nil {
+ return nil, gtserror.Newf("error getting filter ids: %w", err)
+ }
+ return f.GetFiltersByIDs(ctx, filterIDs)
+}
+
func (f *filterDB) populateFilter(ctx context.Context, filter *gtsmodel.Filter) error {
var err error
- errs := gtserror.NewMultiError(2)
+ var errs gtserror.MultiError
- if filter.Keywords == nil {
+ if !filter.KeywordsPopulated() {
// Filter keywords are not set, fetch from the database.
- filter.Keywords, err = f.state.DB.GetFilterKeywordsForFilterID(
- gtscontext.SetBarebones(ctx),
- filter.ID,
- )
+ filter.Keywords, err = f.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs)
if err != nil {
errs.Appendf("error populating filter keywords: %w", err)
}
- for i := range filter.Keywords {
- filter.Keywords[i].Filter = filter
- }
}
- if filter.Statuses == nil {
+ if !filter.StatusesPopulated() {
// Filter statuses are not set, fetch from the database.
- filter.Statuses, err = f.state.DB.GetFilterStatusesForFilterID(
- gtscontext.SetBarebones(ctx),
- filter.ID,
- )
+ filter.Statuses, err = f.GetFilterStatusesByIDs(ctx, filter.StatusIDs)
if err != nil {
errs.Appendf("error populating filter statuses: %w", err)
}
- for i := range filter.Statuses {
- filter.Statuses[i].Filter = filter
- }
}
return errs.Combine()
}
func (f *filterDB) PutFilter(ctx context.Context, filter *gtsmodel.Filter) error {
- // Pre-compile filter keyword regular expressions.
- for _, filterKeyword := range filter.Keywords {
- if err := filterKeyword.Compile(); err != nil {
- return gtserror.Newf("error compiling filter keyword regex: %w", err)
- }
- }
-
- // Update database.
- if err := f.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
- if _, err := tx.
- NewInsert().
- Model(filter).
- Exec(ctx); err != nil {
- return err
- }
-
- if len(filter.Keywords) > 0 {
- if _, err := tx.
- NewInsert().
- Model(&filter.Keywords).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- if len(filter.Statuses) > 0 {
- if _, err := tx.
- NewInsert().
- Model(&filter.Statuses).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- return nil
- }); err != nil {
+ return f.state.Caches.DB.Filter.Store(filter, func() error {
+ _, err := f.db.NewInsert().Model(filter).Exec(ctx)
return err
- }
-
- // Update cache.
- f.state.Caches.DB.Filter.Put(filter)
- f.state.Caches.DB.FilterKeyword.Put(filter.Keywords...)
- f.state.Caches.DB.FilterStatus.Put(filter.Statuses...)
-
- return nil
+ })
}
-func (f *filterDB) UpdateFilter(
- ctx context.Context,
- filter *gtsmodel.Filter,
- filterColumns []string,
- filterKeywordColumns [][]string,
- deleteFilterKeywordIDs []string,
- deleteFilterStatusIDs []string,
-) error {
- if len(filter.Keywords) != len(filterKeywordColumns) {
- return errors.New("number of filter keywords must match number of lists of filter keyword columns")
- }
-
- updatedAt := time.Now()
- filter.UpdatedAt = updatedAt
- for _, filterKeyword := range filter.Keywords {
- filterKeyword.UpdatedAt = updatedAt
- }
- for _, filterStatus := range filter.Statuses {
- filterStatus.UpdatedAt = updatedAt
- }
-
- // If we're updating by column, ensure "updated_at" is included.
- if len(filterColumns) > 0 {
- filterColumns = append(filterColumns, "updated_at")
- }
- for i := range filterKeywordColumns {
- if len(filterKeywordColumns[i]) > 0 {
- filterKeywordColumns[i] = append(filterKeywordColumns[i], "updated_at")
- }
- }
-
- // Pre-compile filter keyword regular expressions.
- for _, filterKeyword := range filter.Keywords {
- if err := filterKeyword.Compile(); err != nil {
- return gtserror.Newf("error compiling filter keyword regex: %w", err)
- }
- }
-
- // Update database.
- if err := f.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
- if _, err := tx.
- NewUpdate().
+func (f *filterDB) UpdateFilter(ctx context.Context, filter *gtsmodel.Filter, cols ...string) error {
+ return f.state.Caches.DB.Filter.Store(filter, func() error {
+ _, err := f.db.NewUpdate().
Model(filter).
- Column(filterColumns...).
Where("? = ?", bun.Ident("id"), filter.ID).
- Exec(ctx); err != nil {
- return err
- }
-
- for i, filterKeyword := range filter.Keywords {
- if _, err := NewUpsert(tx).
- Model(filterKeyword).
- Constraint("id").
- Column(filterKeywordColumns[i]...).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- if len(filter.Statuses) > 0 {
- if _, err := tx.
- NewInsert().
- Ignore().
- Model(&filter.Statuses).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- if len(deleteFilterKeywordIDs) > 0 {
- if _, err := tx.
- NewDelete().
- Model((*gtsmodel.FilterKeyword)(nil)).
- Where("? = (?)", bun.Ident("id"), bun.In(deleteFilterKeywordIDs)).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- if len(deleteFilterStatusIDs) > 0 {
- if _, err := tx.
- NewDelete().
- Model((*gtsmodel.FilterStatus)(nil)).
- Where("? = (?)", bun.Ident("id"), bun.In(deleteFilterStatusIDs)).
- Exec(ctx); err != nil {
- return err
- }
- }
-
- return nil
- }); err != nil {
+ Column(cols...).
+ Exec(ctx)
return err
- }
-
- // Update cache.
- f.state.Caches.DB.Filter.Put(filter)
- f.state.Caches.DB.FilterKeyword.Put(filter.Keywords...)
- f.state.Caches.DB.FilterStatus.Put(filter.Statuses...)
- // TODO: (Vyr) replace with cache multi-invalidate call
- for _, id := range deleteFilterKeywordIDs {
- f.state.Caches.DB.FilterKeyword.Invalidate("ID", id)
- }
- for _, id := range deleteFilterStatusIDs {
- f.state.Caches.DB.FilterStatus.Invalidate("ID", id)
- }
-
- return nil
+ })
}
-func (f *filterDB) DeleteFilterByID(ctx context.Context, id string) error {
+func (f *filterDB) DeleteFilter(ctx context.Context, filter *gtsmodel.Filter) error {
if err := f.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
- // Delete all keywords attached to filter.
+ // Delete all keywords both known
+ // by filter, and possible stragglers,
+ // storing IDs in filter.KeywordIDs.
if _, err := tx.
NewDelete().
Model((*gtsmodel.FilterKeyword)(nil)).
- Where("? = ?", bun.Ident("filter_id"), id).
- Exec(ctx); err != nil {
+ Where("? = ?", bun.Ident("filter_id"), filter.ID).
+ Returning("?", bun.Ident("id")).
+ Exec(ctx, &filter.KeywordIDs); err != nil &&
+ !errors.Is(err, db.ErrNoEntries) {
return err
}
- // Delete all statuses attached to filter.
+ // Delete all statuses both known
+ // by filter, and possible stragglers.
+ // storing IDs in filter.StatusIDs.
if _, err := tx.
NewDelete().
Model((*gtsmodel.FilterStatus)(nil)).
- Where("? = ?", bun.Ident("filter_id"), id).
- Exec(ctx); err != nil {
+ Where("? = ?", bun.Ident("filter_id"), filter.ID).
+ Returning("?", bun.Ident("id")).
+ Exec(ctx, &filter.StatusIDs); err != nil &&
+ !errors.Is(err, db.ErrNoEntries) {
return err
}
- // Delete the filter itself.
+ // Delete filter itself.
_, err := tx.
NewDelete().
Model((*gtsmodel.Filter)(nil)).
- Where("? = ?", bun.Ident("id"), id).
+ Where("? = ?", bun.Ident("id"), filter.ID).
Exec(ctx)
return err
}); err != nil {
return err
}
- // Invalidate this filter.
- f.state.Caches.DB.Filter.Invalidate("ID", id)
-
- // Invalidate all keywords and statuses for this filter.
- f.state.Caches.DB.FilterKeyword.Invalidate("FilterID", id)
- f.state.Caches.DB.FilterStatus.Invalidate("FilterID", id)
+ // Invalidate the filter itself, and
+ // call invalidate hook in-case not cached.
+ f.state.Caches.DB.Filter.Invalidate("ID", filter.ID)
+ f.state.Caches.OnInvalidateFilter(filter)
return nil
}
diff --git a/internal/db/bundb/filter_test.go b/internal/db/bundb/filter_test.go
index 12f3476ed..ce41ac016 100644
--- a/internal/db/bundb/filter_test.go
+++ b/internal/db/bundb/filter_test.go
@@ -38,26 +38,30 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
// Create new example filter with attached keyword.
filter := >smodel.Filter{
- ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
- AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
- Title: "foss jail",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
+ AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
+ Title: "foss jail",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
}
filterKeyword := >smodel.FilterKeyword{
- ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
- AccountID: filter.AccountID,
- FilterID: filter.ID,
- Keyword: "GNU/Linux",
+ ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
+ FilterID: filter.ID,
+ Keyword: "GNU/Linux",
}
filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
+ filter.KeywordIDs = []string{filterKeyword.ID}
// Create new cancellable test context.
ctx := suite.T().Context()
ctx, cncl := context.WithCancel(ctx)
defer cncl()
+ // Insert the example filter keyword into db.
+ if err := suite.db.PutFilterKeyword(ctx, filterKeyword); err != nil {
+ t.Fatalf("error inserting filter keyword: %v", err)
+ }
+
// Insert the example filter into db.
if err := suite.db.PutFilter(ctx, filter); err != nil {
t.Fatalf("error inserting filter: %v", err)
@@ -74,27 +78,18 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
suite.Equal(filter.AccountID, check.AccountID)
suite.Equal(filter.Title, check.Title)
suite.Equal(filter.Action, check.Action)
- suite.Equal(filter.ContextHome, check.ContextHome)
- suite.Equal(filter.ContextNotifications, check.ContextNotifications)
- suite.Equal(filter.ContextPublic, check.ContextPublic)
- suite.Equal(filter.ContextThread, check.ContextThread)
- suite.Equal(filter.ContextAccount, check.ContextAccount)
- suite.NotZero(check.CreatedAt)
- suite.NotZero(check.UpdatedAt)
+ suite.Equal(filter.Contexts, check.Contexts)
suite.Equal(len(filter.Keywords), len(check.Keywords))
suite.Equal(filter.Keywords[0].ID, check.Keywords[0].ID)
- suite.Equal(filter.Keywords[0].AccountID, check.Keywords[0].AccountID)
suite.Equal(filter.Keywords[0].FilterID, check.Keywords[0].FilterID)
suite.Equal(filter.Keywords[0].Keyword, check.Keywords[0].Keyword)
suite.Equal(filter.Keywords[0].FilterID, check.Keywords[0].FilterID)
- suite.NotZero(check.Keywords[0].CreatedAt)
- suite.NotZero(check.Keywords[0].UpdatedAt)
suite.Equal(len(filter.Statuses), len(check.Statuses))
// Fetch all filters.
- all, err := suite.db.GetFiltersForAccountID(ctx, filter.AccountID)
+ all, err := suite.db.GetFiltersByAccountID(ctx, filter.AccountID)
if err != nil {
t.Fatalf("error fetching filters: %v", err)
}
@@ -108,28 +103,39 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
suite.Empty(all[0].Statuses)
- // Update the filter context and add another keyword and a status.
- check.ContextNotifications = util.Ptr(true)
-
+ // Update the filter context and
+ // add another keyword and a status.
+ check.Contexts.SetNotifications()
newKeyword := >smodel.FilterKeyword{
- ID: "01HNEMY810E5XKWDDMN5ZRE749",
- FilterID: filter.ID,
- AccountID: filter.AccountID,
- Keyword: "tux",
+ ID: "01HNEMY810E5XKWDDMN5ZRE749",
+ FilterID: filter.ID,
+ Keyword: "tux",
}
check.Keywords = append(check.Keywords, newKeyword)
-
+ check.KeywordIDs = append(check.KeywordIDs, newKeyword.ID)
newStatus := >smodel.FilterStatus{
- ID: "01HNEMYD5XE7C8HH8TNCZ76FN2",
- FilterID: filter.ID,
- AccountID: filter.AccountID,
- StatusID: "01HNEKZW34SQZ8PSDQ0Z10NZES",
+ ID: "01HNEMYD5XE7C8HH8TNCZ76FN2",
+ FilterID: filter.ID,
+ StatusID: "01HNEKZW34SQZ8PSDQ0Z10NZES",
}
check.Statuses = append(check.Statuses, newStatus)
+ check.StatusIDs = append(check.StatusIDs, newStatus.ID)
- if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{nil, nil}, nil, nil); err != nil {
+ // Insert the new filter keyword.
+ if err := suite.db.PutFilterKeyword(ctx, newKeyword); err != nil {
+ t.Fatalf("error inserting filter keyword: %v", err)
+ }
+
+ // Insert the new filter status.
+ if err := suite.db.PutFilterStatus(ctx, newStatus); err != nil {
+ t.Fatalf("error inserting filter status: %v", err)
+ }
+
+ // Now update the filter with new keyword and status.
+ if err := suite.db.UpdateFilter(ctx, check); err != nil {
t.Fatalf("error updating filter: %v", err)
}
+
// Now fetch newly updated filter.
check, err = suite.db.GetFilterByID(ctx, filter.ID)
if err != nil {
@@ -137,22 +143,11 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
}
// Ensure expected fields were modified on check filter.
- suite.True(check.UpdatedAt.After(filter.UpdatedAt))
- if suite.NotNil(check.ContextHome) {
- suite.True(*check.ContextHome)
- }
- if suite.NotNil(check.ContextNotifications) {
- suite.True(*check.ContextNotifications)
- }
- if suite.NotNil(check.ContextPublic) {
- suite.True(*check.ContextPublic)
- }
- if suite.NotNil(check.ContextThread) {
- suite.False(*check.ContextThread)
- }
- if suite.NotNil(check.ContextAccount) {
- suite.False(*check.ContextAccount)
- }
+ suite.True(check.Contexts.Home())
+ suite.True(check.Contexts.Notifications())
+ suite.True(check.Contexts.Public())
+ suite.False(check.Contexts.Thread())
+ suite.False(check.Contexts.Account())
// Ensure keyword entries were added.
suite.Len(check.Keywords, 2)
@@ -175,9 +170,19 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
check.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
check.Statuses = nil
- if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{{"whole_word"}}, []string{newKeyword.ID}, nil); err != nil {
+ // Update the original filter keyword.
+ filterKeyword.WholeWord = util.Ptr(true)
+ if err := suite.db.UpdateFilterKeyword(ctx, filterKeyword); err != nil {
+ t.Fatalf("error updating filter keyword: %v", err)
+ }
+
+ // Drop most recently added filter keyword from filter.
+ check.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
+ check.KeywordIDs = []string{filterKeyword.ID}
+ if err := suite.db.UpdateFilter(ctx, check); err != nil {
t.Fatalf("error updating filter: %v", err)
}
+
check, err = suite.db.GetFilterByID(ctx, filter.ID)
if err != nil {
t.Fatalf("error fetching updated filter: %v", err)
@@ -186,23 +191,14 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
// Ensure expected fields were not modified.
suite.Equal(filter.Title, check.Title)
suite.Equal(gtsmodel.FilterActionWarn, check.Action)
- if suite.NotNil(check.ContextHome) {
- suite.True(*check.ContextHome)
- }
- if suite.NotNil(check.ContextNotifications) {
- suite.True(*check.ContextNotifications)
- }
- if suite.NotNil(check.ContextPublic) {
- suite.True(*check.ContextPublic)
- }
- if suite.NotNil(check.ContextThread) {
- suite.False(*check.ContextThread)
- }
- if suite.NotNil(check.ContextAccount) {
- suite.False(*check.ContextAccount)
- }
+ suite.True(check.Contexts.Home())
+ suite.True(check.Contexts.Notifications())
+ suite.True(check.Contexts.Public())
+ suite.False(check.Contexts.Thread())
+ suite.False(check.Contexts.Account())
- // Ensure only changed field of keyword was modified, and other keyword was deleted.
+ // Ensure only changed field of keyword was
+ // modified, and other keyword was deleted.
suite.Len(check.Keywords, 1)
suite.Equal(filterKeyword.ID, check.Keywords[0].ID)
suite.Equal("GNU/Linux", check.Keywords[0].Keyword)
@@ -214,29 +210,8 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
suite.Len(check.Statuses, 1)
suite.Equal(newStatus.ID, check.Statuses[0].ID)
- // Add another status entry for the same status ID. It should be ignored without problems.
- redundantStatus := >smodel.FilterStatus{
- ID: "01HQXJ5Y405XZSQ67C2BSQ6HJ0",
- FilterID: filter.ID,
- AccountID: filter.AccountID,
- StatusID: newStatus.StatusID,
- }
- check.Statuses = []*gtsmodel.FilterStatus{redundantStatus}
- if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{nil}, nil, nil); err != nil {
- t.Fatalf("error updating filter: %v", err)
- }
- check, err = suite.db.GetFilterByID(ctx, filter.ID)
- if err != nil {
- t.Fatalf("error fetching updated filter: %v", err)
- }
-
- // Ensure status entry was not deleted, updated, or duplicated.
- suite.Len(check.Statuses, 1)
- suite.Equal(newStatus.ID, check.Statuses[0].ID)
- suite.Equal(newStatus.StatusID, check.Statuses[0].StatusID)
-
// Now delete the filter from the DB.
- if err := suite.db.DeleteFilterByID(ctx, filter.ID); err != nil {
+ if err := suite.db.DeleteFilter(ctx, filter); err != nil {
t.Fatalf("error deleting filter: %v", err)
}
@@ -256,11 +231,11 @@ func (suite *FilterTestSuite) TestFilterTitleOverlap() {
// Create an empty filter for account 1.
account1filter1 := >smodel.Filter{
- ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
- AccountID: account1,
- Title: "my filter",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
+ ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
+ AccountID: account1,
+ Title: "my filter",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
if err := suite.db.PutFilter(ctx, account1filter1); err != nil {
suite.FailNow("", "error putting account1filter1: %s", err)
@@ -269,11 +244,11 @@ func (suite *FilterTestSuite) TestFilterTitleOverlap() {
// Create a filter for account 2 with
// the same title, should be no issue.
account2filter1 := >smodel.Filter{
- ID: "01JAG5GPXG7H5Y4ZP78GV1F2ET",
- AccountID: account2,
- Title: "my filter",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
+ ID: "01JAG5GPXG7H5Y4ZP78GV1F2ET",
+ AccountID: account2,
+ Title: "my filter",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
if err := suite.db.PutFilter(ctx, account2filter1); err != nil {
suite.FailNow("", "error putting account2filter1: %s", err)
@@ -283,11 +258,11 @@ func (suite *FilterTestSuite) TestFilterTitleOverlap() {
// account 1 with the same name as
// an existing filter of theirs.
account1filter2 := >smodel.Filter{
- ID: "01JAG5J8NYKQE2KYCD28Y4P05V",
- AccountID: account1,
- Title: "my filter",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
+ ID: "01JAG5J8NYKQE2KYCD28Y4P05V",
+ AccountID: account1,
+ Title: "my filter",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
err := suite.db.PutFilter(ctx, account1filter2)
if !errors.Is(err, db.ErrAlreadyExists) {
diff --git a/internal/db/bundb/filterkeyword.go b/internal/db/bundb/filterkeyword.go
index 1c80061e9..1476245b0 100644
--- a/internal/db/bundb/filterkeyword.go
+++ b/internal/db/bundb/filterkeyword.go
@@ -20,9 +20,7 @@ package bundb
import (
"context"
"slices"
- "time"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -31,7 +29,7 @@ import (
)
func (f *filterDB) GetFilterKeywordByID(ctx context.Context, id string) (*gtsmodel.FilterKeyword, error) {
- filterKeyword, err := f.state.Caches.DB.FilterKeyword.LoadOne(
+ return f.state.Caches.DB.FilterKeyword.LoadOne(
"ID",
func() (*gtsmodel.FilterKeyword, error) {
var filterKeyword gtsmodel.FilterKeyword
@@ -54,64 +52,16 @@ func (f *filterDB) GetFilterKeywordByID(ctx context.Context, id string) (*gtsmod
},
id,
)
- if err != nil {
- return nil, err
- }
-
- if !gtscontext.Barebones(ctx) {
- err = f.populateFilterKeyword(ctx, filterKeyword)
- if err != nil {
- return nil, err
- }
- }
-
- return filterKeyword, nil
}
-func (f *filterDB) populateFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (err error) {
- if filterKeyword.Filter == nil {
- // Filter is not set, fetch from the cache or database.
- filterKeyword.Filter, err = f.state.DB.GetFilterByID(
-
- // Don't populate the filter with all of its keywords
- // and statuses or we'll just end up back here.
- gtscontext.SetBarebones(ctx),
- filterKeyword.FilterID,
- )
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func (f *filterDB) GetFilterKeywordsForFilterID(ctx context.Context, filterID string) ([]*gtsmodel.FilterKeyword, error) {
- return f.getFilterKeywords(ctx, "filter_id", filterID)
-}
-
-func (f *filterDB) GetFilterKeywordsForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.FilterKeyword, error) {
- return f.getFilterKeywords(ctx, "account_id", accountID)
-}
-
-func (f *filterDB) getFilterKeywords(ctx context.Context, idColumn string, id string) ([]*gtsmodel.FilterKeyword, error) {
- var filterKeywordIDs []string
-
- if err := f.db.
- NewSelect().
- Model((*gtsmodel.FilterKeyword)(nil)).
- Column("id").
- Where("? = ?", bun.Ident(idColumn), id).
- Scan(ctx, &filterKeywordIDs); err != nil {
- return nil, err
- }
-
- if len(filterKeywordIDs) == 0 {
+func (f *filterDB) GetFilterKeywordsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.FilterKeyword, error) {
+ if len(ids) == 0 {
return nil, nil
}
// Get each filter keyword by ID from the cache or DB.
filterKeywords, err := f.state.Caches.DB.FilterKeyword.LoadIDs("ID",
- filterKeywordIDs,
+ ids,
func(uncached []string) ([]*gtsmodel.FilterKeyword, error) {
filterKeywords := make([]*gtsmodel.FilterKeyword, 0, len(uncached))
@@ -140,23 +90,10 @@ func (f *filterDB) getFilterKeywords(ctx context.Context, idColumn string, id st
}
// Put the filter keyword structs in the same order as the filter keyword IDs.
- xslices.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
+ xslices.OrderBy(filterKeywords, ids, func(filterKeyword *gtsmodel.FilterKeyword) string {
return filterKeyword.ID
})
- if gtscontext.Barebones(ctx) {
- return filterKeywords, nil
- }
-
- // Populate the filter keywords. Remove any that we can't populate from the return slice.
- filterKeywords = slices.DeleteFunc(filterKeywords, func(filterKeyword *gtsmodel.FilterKeyword) bool {
- if err := f.populateFilterKeyword(ctx, filterKeyword); err != nil {
- log.Errorf(ctx, "error populating filter keyword: %v", err)
- return true
- }
- return false
- })
-
return filterKeywords, nil
}
@@ -178,11 +115,7 @@ func (f *filterDB) PutFilterKeyword(ctx context.Context, filterKeyword *gtsmodel
})
}
-func (f *filterDB) UpdateFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword, columns ...string) error {
- filterKeyword.UpdatedAt = time.Now()
- if len(columns) > 0 {
- columns = append(columns, "updated_at")
- }
+func (f *filterDB) UpdateFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword, cols ...string) error {
if filterKeyword.Regexp == nil {
// Ensure regexp is compiled
// before attempted caching.
@@ -196,22 +129,20 @@ func (f *filterDB) UpdateFilterKeyword(ctx context.Context, filterKeyword *gtsmo
NewUpdate().
Model(filterKeyword).
Where("? = ?", bun.Ident("id"), filterKeyword.ID).
- Column(columns...).
+ Column(cols...).
Exec(ctx)
return err
})
}
-func (f *filterDB) DeleteFilterKeywordByID(ctx context.Context, id string) error {
+func (f *filterDB) DeleteFilterKeywordsByIDs(ctx context.Context, ids ...string) error {
if _, err := f.db.
NewDelete().
Model((*gtsmodel.FilterKeyword)(nil)).
- Where("? = ?", bun.Ident("id"), id).
+ Where("? IN (?)", bun.Ident("id"), bun.In(ids)).
Exec(ctx); err != nil {
return err
}
-
- f.state.Caches.DB.FilterKeyword.Invalidate("ID", id)
-
+ f.state.Caches.DB.FilterKeyword.InvalidateIDs("ID", ids)
return nil
}
diff --git a/internal/db/bundb/filterkeyword_test.go b/internal/db/bundb/filterkeyword_test.go
index ab814d413..bce308f42 100644
--- a/internal/db/bundb/filterkeyword_test.go
+++ b/internal/db/bundb/filterkeyword_test.go
@@ -32,12 +32,11 @@ func (suite *FilterTestSuite) TestFilterKeywordCRUD() {
// Create new filter.
filter := >smodel.Filter{
- ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
- AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
- Title: "foss jail",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
+ AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
+ Title: "foss jail",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
}
// Create new cancellable test context.
@@ -51,19 +50,11 @@ func (suite *FilterTestSuite) TestFilterKeywordCRUD() {
t.Fatalf("error inserting filter: %v", err)
}
- // There should be no filter keywords yet.
- all, err := suite.db.GetFilterKeywordsForAccountID(ctx, filter.AccountID)
- if err != nil {
- t.Fatalf("error fetching filter keywords: %v", err)
- }
- suite.Empty(all)
-
// Add a filter keyword to it.
filterKeyword := >smodel.FilterKeyword{
- ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
- AccountID: filter.AccountID,
- FilterID: filter.ID,
- Keyword: "GNU/Linux",
+ ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
+ FilterID: filter.ID,
+ Keyword: "GNU/Linux",
}
// Insert the new filter keyword into the DB.
@@ -78,28 +69,17 @@ func (suite *FilterTestSuite) TestFilterKeywordCRUD() {
t.Fatalf("error fetching filter keyword: %v", err)
}
suite.Equal(filterKeyword.ID, check.ID)
- suite.NotZero(check.CreatedAt)
- suite.NotZero(check.UpdatedAt)
- suite.Equal(filterKeyword.AccountID, check.AccountID)
suite.Equal(filterKeyword.FilterID, check.FilterID)
suite.Equal(filterKeyword.Keyword, check.Keyword)
suite.Equal(filterKeyword.WholeWord, check.WholeWord)
- // Loading filter keywords by account ID should find the one we inserted.
- all, err = suite.db.GetFilterKeywordsForAccountID(ctx, filter.AccountID)
+ // Check that fetching multiple filter keywords by IDs works.
+ checks, err := suite.db.GetFilterKeywordsByIDs(ctx, []string{filterKeyword.ID})
if err != nil {
t.Fatalf("error fetching filter keywords: %v", err)
}
- suite.Len(all, 1)
- suite.Equal(filterKeyword.ID, all[0].ID)
-
- // Loading filter keywords by filter ID should also find the one we inserted.
- all, err = suite.db.GetFilterKeywordsForFilterID(ctx, filter.ID)
- if err != nil {
- t.Fatalf("error fetching filter keywords: %v", err)
- }
- suite.Len(all, 1)
- suite.Equal(filterKeyword.ID, all[0].ID)
+ suite.Len(checks, 1)
+ suite.Equal(filterKeyword.ID, checks[0].ID)
// Modify the filter keyword.
filterKeyword.WholeWord = util.Ptr(true)
@@ -114,15 +94,12 @@ func (suite *FilterTestSuite) TestFilterKeywordCRUD() {
t.Fatalf("error fetching filter keyword: %v", err)
}
suite.Equal(filterKeyword.ID, check.ID)
- suite.NotZero(check.CreatedAt)
- suite.True(check.UpdatedAt.After(check.CreatedAt))
- suite.Equal(filterKeyword.AccountID, check.AccountID)
suite.Equal(filterKeyword.FilterID, check.FilterID)
suite.Equal(filterKeyword.Keyword, check.Keyword)
suite.Equal(filterKeyword.WholeWord, check.WholeWord)
// Delete the filter keyword from the DB.
- err = suite.db.DeleteFilterKeywordByID(ctx, filter.ID)
+ err = suite.db.DeleteFilterKeywordsByIDs(ctx, filter.ID)
if err != nil {
t.Fatalf("error deleting filter keyword: %v", err)
}
diff --git a/internal/db/bundb/filterstatus.go b/internal/db/bundb/filterstatus.go
index a14e2a7b4..d15705a14 100644
--- a/internal/db/bundb/filterstatus.go
+++ b/internal/db/bundb/filterstatus.go
@@ -19,86 +19,38 @@ package bundb
import (
"context"
- "slices"
- "time"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/util/xslices"
"github.com/uptrace/bun"
)
func (f *filterDB) GetFilterStatusByID(ctx context.Context, id string) (*gtsmodel.FilterStatus, error) {
- filterStatus, err := f.state.Caches.DB.FilterStatus.LoadOne(
+ return f.state.Caches.DB.FilterStatus.LoadOne(
"ID",
func() (*gtsmodel.FilterStatus, error) {
var filterStatus gtsmodel.FilterStatus
- err := f.db.
+ if err := f.db.
NewSelect().
Model(&filterStatus).
Where("? = ?", bun.Ident("id"), id).
- Scan(ctx)
- return &filterStatus, err
+ Scan(ctx); err != nil {
+ return nil, err
+ }
+ return &filterStatus, nil
},
id,
)
- if err != nil {
- return nil, err
- }
-
- if !gtscontext.Barebones(ctx) {
- err = f.populateFilterStatus(ctx, filterStatus)
- if err != nil {
- return nil, err
- }
- }
-
- return filterStatus, nil
}
-func (f *filterDB) populateFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus) error {
- if filterStatus.Filter == nil {
- // Filter is not set, fetch from the cache or database.
- filter, err := f.state.DB.GetFilterByID(
- // Don't populate the filter with all of its keywords and statuses or we'll just end up back here.
- gtscontext.SetBarebones(ctx),
- filterStatus.FilterID,
- )
- if err != nil {
- return err
- }
- filterStatus.Filter = filter
- }
-
- return nil
-}
-
-func (f *filterDB) GetFilterStatusesForFilterID(ctx context.Context, filterID string) ([]*gtsmodel.FilterStatus, error) {
- return f.getFilterStatuses(ctx, "filter_id", filterID)
-}
-
-func (f *filterDB) GetFilterStatusesForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.FilterStatus, error) {
- return f.getFilterStatuses(ctx, "account_id", accountID)
-}
-
-func (f *filterDB) getFilterStatuses(ctx context.Context, idColumn string, id string) ([]*gtsmodel.FilterStatus, error) {
- var filterStatusIDs []string
- if err := f.db.
- NewSelect().
- Model((*gtsmodel.FilterStatus)(nil)).
- Column("id").
- Where("? = ?", bun.Ident(idColumn), id).
- Scan(ctx, &filterStatusIDs); err != nil {
- return nil, err
- }
- if len(filterStatusIDs) == 0 {
+func (f *filterDB) GetFilterStatusesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.FilterStatus, error) {
+ if len(ids) == 0 {
return nil, nil
}
// Get each filter status by ID from the cache or DB.
filterStatuses, err := f.state.Caches.DB.FilterStatus.LoadIDs("ID",
- filterStatusIDs,
+ ids,
func(uncached []string) ([]*gtsmodel.FilterStatus, error) {
filterStatuses := make([]*gtsmodel.FilterStatus, 0, len(uncached))
if err := f.db.
@@ -116,29 +68,11 @@ func (f *filterDB) getFilterStatuses(ctx context.Context, idColumn string, id st
}
// Put the filter status structs in the same order as the filter status IDs.
- xslices.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
+ xslices.OrderBy(filterStatuses, ids, func(filterStatus *gtsmodel.FilterStatus) string {
return filterStatus.ID
})
- if gtscontext.Barebones(ctx) {
- return filterStatuses, nil
- }
-
- // Populate the filter statuses. Remove any that we can't populate from the return slice.
- errs := gtserror.NewMultiError(len(filterStatuses))
- filterStatuses = slices.DeleteFunc(filterStatuses, func(filterStatus *gtsmodel.FilterStatus) bool {
- if err := f.populateFilterStatus(ctx, filterStatus); err != nil {
- errs.Appendf(
- "error populating filter status %s: %w",
- filterStatus.ID,
- err,
- )
- return true
- }
- return false
- })
-
- return filterStatuses, errs.Combine()
+ return filterStatuses, nil
}
func (f *filterDB) PutFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus) error {
@@ -152,11 +86,6 @@ func (f *filterDB) PutFilterStatus(ctx context.Context, filterStatus *gtsmodel.F
}
func (f *filterDB) UpdateFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus, columns ...string) error {
- filterStatus.UpdatedAt = time.Now()
- if len(columns) > 0 {
- columns = append(columns, "updated_at")
- }
-
return f.state.Caches.DB.FilterStatus.Store(filterStatus, func() error {
_, err := f.db.
NewUpdate().
@@ -168,16 +97,14 @@ func (f *filterDB) UpdateFilterStatus(ctx context.Context, filterStatus *gtsmode
})
}
-func (f *filterDB) DeleteFilterStatusByID(ctx context.Context, id string) error {
+func (f *filterDB) DeleteFilterStatusesByIDs(ctx context.Context, ids ...string) error {
if _, err := f.db.
NewDelete().
Model((*gtsmodel.FilterStatus)(nil)).
- Where("? = ?", bun.Ident("id"), id).
+ Where("? IN (?)", bun.Ident("id"), bun.In(ids)).
Exec(ctx); err != nil {
return err
}
-
- f.state.Caches.DB.FilterStatus.Invalidate("ID", id)
-
+ f.state.Caches.DB.FilterStatus.InvalidateIDs("ID", ids)
return nil
}
diff --git a/internal/db/bundb/filterstatus_test.go b/internal/db/bundb/filterstatus_test.go
index 9485ee3f7..27f5c17b3 100644
--- a/internal/db/bundb/filterstatus_test.go
+++ b/internal/db/bundb/filterstatus_test.go
@@ -23,7 +23,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
- "code.superseriousbusiness.org/gotosocial/internal/util"
)
// TestFilterStatusCRD tests CRD (no U) and read-all operations on filter statuses.
@@ -32,12 +31,11 @@ func (suite *FilterTestSuite) TestFilterStatusCRD() {
// Create new filter.
filter := >smodel.Filter{
- ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
- AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
- Title: "foss jail",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
+ AccountID: "01HNEJXCPRTJVJY9MV0VVHGD47",
+ Title: "foss jail",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
}
// Create new cancellable test context.
@@ -51,19 +49,11 @@ func (suite *FilterTestSuite) TestFilterStatusCRD() {
t.Fatalf("error inserting filter: %v", err)
}
- // There should be no filter statuses yet.
- all, err := suite.db.GetFilterStatusesForAccountID(ctx, filter.AccountID)
- if err != nil {
- t.Fatalf("error fetching filter statuses: %v", err)
- }
- suite.Empty(all)
-
// Add a filter status to it.
filterStatus := >smodel.FilterStatus{
- ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
- AccountID: filter.AccountID,
- FilterID: filter.ID,
- StatusID: "01HQXGMQ3QFXRT4GX9WNQ8KC0X",
+ ID: "01HNEK4RW5QEAMG9Y4ET6ST0J4",
+ FilterID: filter.ID,
+ StatusID: "01HQXGMQ3QFXRT4GX9WNQ8KC0X",
}
// Insert the new filter status into the DB.
@@ -78,30 +68,19 @@ func (suite *FilterTestSuite) TestFilterStatusCRD() {
t.Fatalf("error fetching filter status: %v", err)
}
suite.Equal(filterStatus.ID, check.ID)
- suite.NotZero(check.CreatedAt)
- suite.NotZero(check.UpdatedAt)
- suite.Equal(filterStatus.AccountID, check.AccountID)
suite.Equal(filterStatus.FilterID, check.FilterID)
suite.Equal(filterStatus.StatusID, check.StatusID)
- // Loading filter statuses by account ID should find the one we inserted.
- all, err = suite.db.GetFilterStatusesForAccountID(ctx, filter.AccountID)
+ // Check that fetching multiple filter statuses by IDs works.
+ checks, err := suite.db.GetFilterStatusesByIDs(ctx, []string{filterStatus.ID})
if err != nil {
t.Fatalf("error fetching filter statuses: %v", err)
}
- suite.Len(all, 1)
- suite.Equal(filterStatus.ID, all[0].ID)
-
- // Loading filter statuses by filter ID should also find the one we inserted.
- all, err = suite.db.GetFilterStatusesForFilterID(ctx, filter.ID)
- if err != nil {
- t.Fatalf("error fetching filter statuses: %v", err)
- }
- suite.Len(all, 1)
- suite.Equal(filterStatus.ID, all[0].ID)
+ suite.Len(checks, 1)
+ suite.Equal(filterStatus.ID, checks[0].ID)
// Delete the filter status from the DB.
- err = suite.db.DeleteFilterStatusByID(ctx, filter.ID)
+ err = suite.db.DeleteFilterStatusesByIDs(ctx, filter.ID)
if err != nil {
t.Fatalf("error deleting filter status: %v", err)
}
diff --git a/internal/db/bundb/migrations/20241018151036_filter_unique_fix.go b/internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
index a1eb700a7..b7b185f99 100644
--- a/internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
+++ b/internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
@@ -20,7 +20,7 @@ package migrations
import (
"context"
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ gtsmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20241018151036_filter_unique_fix"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
diff --git a/internal/db/bundb/migrations/20241018151036_filter_unique_fix/filter.go b/internal/db/bundb/migrations/20241018151036_filter_unique_fix/filter.go
new file mode 100644
index 000000000..e90eabaac
--- /dev/null
+++ b/internal/db/bundb/migrations/20241018151036_filter_unique_fix/filter.go
@@ -0,0 +1,77 @@
+// 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 .
+
+package gtsmodel
+
+import (
+ "regexp"
+ "time"
+)
+
+// 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.
+}
+
+// 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
+}
+
+// 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.
+}
+
+// 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"
+)
diff --git a/internal/db/bundb/migrations/20250617122055_filter_improvements.go b/internal/db/bundb/migrations/20250617122055_filter_improvements.go
new file mode 100644
index 000000000..09fde089e
--- /dev/null
+++ b/internal/db/bundb/migrations/20250617122055_filter_improvements.go
@@ -0,0 +1,303 @@
+// 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 .
+
+package migrations
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "reflect"
+ "strings"
+
+ oldmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20241018151036_filter_unique_fix"
+ newmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250617122055_filter_improvements"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ // Replace 'context_*' and 'action' columns with space-saving enum / bitfields.
+ if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ newFilterType := reflect.TypeOf((*newmodel.Filter)(nil))
+
+ // Generate bun definition for new filter table contexts column.
+ newColDef, err := getBunColumnDef(tx, newFilterType, "Contexts")
+ if err != nil {
+ return gtserror.Newf("error getting bun column def: %w", err)
+ }
+
+ // Add new column type to table.
+ if _, err := tx.NewAddColumn().
+ Model((*oldmodel.Filter)(nil)).
+ ColumnExpr(newColDef).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error adding filter.contexts column: %w", err)
+ }
+
+ // Generate bun definition for new filter table action column.
+ newColDef, err = getBunColumnDef(tx, newFilterType, "Action")
+ if err != nil {
+ return gtserror.Newf("error getting bun column def: %w", err)
+ }
+
+ // For now, name it as '_new'.
+ newColDef = strings.ReplaceAll(
+ newColDef,
+ "action",
+ "action_new",
+ )
+
+ // Add new column type to table.
+ if _, err := tx.NewAddColumn().
+ Model((*oldmodel.Filter)(nil)).
+ ColumnExpr(newColDef).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error adding filter.contexts column: %w", err)
+ }
+
+ var oldFilters []*oldmodel.Filter
+
+ // Select all filters.
+ if err := tx.NewSelect().
+ Model(&oldFilters).
+ Column("id",
+ "context_home",
+ "context_notifications",
+ "context_public",
+ "context_thread",
+ "context_account",
+ "action").
+ Scan(ctx); err != nil {
+ return gtserror.Newf("error selecting filters: %w", err)
+ }
+
+ for _, oldFilter := range oldFilters {
+ var newContexts newmodel.FilterContexts
+ var newAction newmodel.FilterAction
+
+ // Convert old contexts
+ // to new contexts type.
+ if *oldFilter.ContextHome {
+ newContexts.SetHome()
+ }
+ if *oldFilter.ContextNotifications {
+ newContexts.SetNotifications()
+ }
+ if *oldFilter.ContextPublic {
+ newContexts.SetPublic()
+ }
+ if *oldFilter.ContextThread {
+ newContexts.SetThread()
+ }
+ if *oldFilter.ContextAccount {
+ newContexts.SetAccount()
+ }
+
+ // Convert old action
+ // to new action type.
+ switch oldFilter.Action {
+ case oldmodel.FilterActionHide:
+ newAction = newmodel.FilterActionHide
+ case oldmodel.FilterActionWarn:
+ newAction = newmodel.FilterActionWarn
+ default:
+ return gtserror.Newf("invalid filter action %q for %s", oldFilter.Action, oldFilter.ID)
+ }
+
+ // Update filter row with
+ // the new contexts value.
+ if _, err := tx.NewUpdate().
+ Model((*oldmodel.Filter)(nil)).
+ Where("? = ?", bun.Ident("id"), oldFilter.ID).
+ Set("? = ?", bun.Ident("contexts"), newContexts).
+ Set("? = ?", bun.Ident("action_new"), newAction).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error updating filter.contexts: %w", err)
+ }
+ }
+
+ // Drop the old updated columns.
+ for _, col := range []string{
+ "context_home",
+ "context_notifications",
+ "context_public",
+ "context_thread",
+ "context_account",
+ "action",
+ } {
+ if _, err := tx.NewDropColumn().
+ Model((*oldmodel.Filter)(nil)).
+ Column(col).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error dropping filter.%s column: %w", col, err)
+ }
+ }
+
+ // Rename the new action
+ // column to correct name.
+ if _, err := tx.NewRaw(
+ "ALTER TABLE ? RENAME COLUMN ? TO ?",
+ bun.Ident("filters"),
+ bun.Ident("action_new"),
+ bun.Ident("action"),
+ ).Exec(ctx); err != nil {
+ return gtserror.Newf("error renaming new action column: %w", err)
+ }
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ // SQLITE: force WAL checkpoint to merge writes.
+ if err := doWALCheckpoint(ctx, db); err != nil {
+ return err
+ }
+
+ // Drop a bunch of (now, and more generally) unused columns from filter tables.
+ if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ for model, indices := range map[any][]string{
+ (*oldmodel.FilterKeyword)(nil): {"filter_keywords_account_id_idx"},
+ (*oldmodel.FilterStatus)(nil): {"filter_statuses_account_id_idx"},
+ } {
+ for _, index := range indices {
+ if _, err := tx.NewDropIndex().
+ Model(model).
+ Index(index).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error dropping %s index: %w", index, err)
+ }
+ }
+ }
+ for model, cols := range map[any][]string{
+ (*oldmodel.Filter)(nil): {"created_at", "updated_at"},
+ (*oldmodel.FilterKeyword)(nil): {"created_at", "updated_at", "account_id"},
+ (*oldmodel.FilterStatus)(nil): {"created_at", "updated_at", "account_id"},
+ } {
+ for _, col := range cols {
+ if _, err := tx.NewDropColumn().
+ Model(model).
+ Column(col).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error dropping %T.%s column: %w", model, col, err)
+ }
+ }
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ // SQLITE: force WAL checkpoint to merge writes.
+ if err := doWALCheckpoint(ctx, db); err != nil {
+ return err
+ }
+
+ // Create links from 'filters' table to 'filter_{keywords,statuses}' tables.
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ newFilterType := reflect.TypeOf((*newmodel.Filter)(nil))
+
+ var filterIDs string
+
+ // Select all filter IDs.
+ if err := tx.NewSelect().
+ Model((*newmodel.Filter)(nil)).
+ Column("id").
+ Scan(ctx, &filterIDs); err != nil && !errors.Is(err, sql.ErrNoRows) {
+ return gtserror.Newf("error selecting filter ids: %w", err)
+ }
+
+ for _, data := range []struct {
+ Field string
+ Model any
+ }{
+ {
+ Field: "KeywordIDs",
+ Model: (*newmodel.FilterKeyword)(nil),
+ },
+ {
+ Field: "StatusIDs",
+ Model: (*newmodel.FilterStatus)(nil),
+ },
+ } {
+ // Generate bun definition for new filter table field column.
+ newColDef, err := getBunColumnDef(tx, newFilterType, data.Field)
+ if err != nil {
+ return gtserror.Newf("error getting bun column def: %w", err)
+ }
+
+ // Add new column type to table.
+ if _, err := tx.NewAddColumn().
+ Model((*oldmodel.Filter)(nil)).
+ ColumnExpr(newColDef).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error adding filter.%s column: %w", data.Field, err)
+ }
+
+ // Get the SQL field information from bun for Filter{}.$Field.
+ field, _, err := getModelField(tx, newFilterType, data.Field)
+ if err != nil {
+ return gtserror.Newf("error getting bun model field: %w", err)
+ }
+
+ // Extract column name.
+ col := field.SQLName
+
+ var relatedIDs []string
+ for _, filterID := range filterIDs {
+ // Reset related IDs.
+ clear(relatedIDs)
+ relatedIDs = relatedIDs[:0]
+
+ // Select $Model IDs that
+ // are attached to filterID.
+ if err := tx.NewSelect().
+ Model(data.Model).
+ Column("id").
+ Where("? = ?", bun.Ident("filter_id"), filterID).
+ Scan(ctx, &relatedIDs); err != nil {
+ return gtserror.Newf("error selecting %T ids: %w", data.Model, err)
+ }
+
+ // Now update the relevant filter
+ // row to contain these related IDs.
+ if _, err := tx.NewUpdate().
+ Model((*newmodel.Filter)(nil)).
+ Where("? = ?", bun.Ident("id"), filterID).
+ Set("? = ?", bun.Ident(col), relatedIDs).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error updating filters.%s ids: %w", col, err)
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/migrations/20250617122055_filter_improvements/filter.go b/internal/db/bundb/migrations/20250617122055_filter_improvements/filter.go
new file mode 100644
index 000000000..20d3ba32e
--- /dev/null
+++ b/internal/db/bundb/migrations/20250617122055_filter_improvements/filter.go
@@ -0,0 +1,243 @@
+// 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 .
+
+package gtsmodel
+
+import (
+ "regexp"
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+)
+
+// smallint is the largest size supported
+// by a PostgreSQL SMALLINT, since an SQLite
+// SMALLINT is actually variable in size.
+type smallint int16
+
+// enumType is the type we (at least, should) use
+// for database enum types, as smallest int size.
+type enumType smallint
+
+// bitFieldType is the type we use
+// for database int bit fields, at
+// least where the smallest int size
+// will suffice for number of fields.
+type bitFieldType smallint
+
+// 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
+)
+
+// 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 {
+ switch ctx {
+ case FilterContextHome:
+ return ctxs.Home()
+ case FilterContextNotifications:
+ return ctxs.Notifications()
+ case FilterContextPublic:
+ return ctxs.Public()
+ case FilterContextThread:
+ return ctxs.Thread()
+ case FilterContextAccount:
+ return ctxs.Account()
+ default:
+ return false
+ }
+}
+
+// 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)
+}
+
+// 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
+)
+
+// 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?
+}
+
+// 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.
+}
diff --git a/internal/db/filter.go b/internal/db/filter.go
index 4962aa1de..89c527a68 100644
--- a/internal/db/filter.go
+++ b/internal/db/filter.go
@@ -23,81 +23,51 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
-// Filter contains methods for creating, reading, updating, and deleting filters and their keyword and status entries.
+// Filter contains methods for creating, reading, updating,
+// and deleting filters and their keyword and status entries.
type Filter interface {
- //
// GetFilterByID gets one filter with the given id.
GetFilterByID(ctx context.Context, id string) (*gtsmodel.Filter, error)
- // GetFiltersForAccountID gets all filters owned by the given accountID.
- GetFiltersForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error)
+ // GetFiltersByAccountID gets all filters owned by the given accountID.
+ GetFiltersByAccountID(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error)
// PutFilter puts a new filter in the database, adding any attached keywords or statuses.
// It uses a transaction to ensure no partial updates.
PutFilter(ctx context.Context, filter *gtsmodel.Filter) error
- // UpdateFilter updates the given filter,
- // upserts any attached keywords and inserts any new statuses (existing statuses cannot be updated),
- // and deletes indicated filter keywords and statuses by ID.
- // It uses a transaction to ensure no partial updates.
- // The column lists are optional; if not specified, all columns will be updated.
- // The filter keyword columns list is *per keyword*.
- // To update all keyword columns, provide a list where every element is an empty list.
- UpdateFilter(
- ctx context.Context,
- filter *gtsmodel.Filter,
- filterColumns []string,
- filterKeywordColumns [][]string,
- deleteFilterKeywordIDs []string,
- deleteFilterStatusIDs []string,
- ) error
+ // UpdateFilter ...
+ UpdateFilter(ctx context.Context, filter *gtsmodel.Filter, cols ...string) error
- // DeleteFilterByID deletes one filter with the given ID.
- // It uses a transaction to ensure no partial updates.
- DeleteFilterByID(ctx context.Context, id string) error
-
- //
-
- //
+ // DeleteFilter deletes the given filter and all associated FilterKeyword{}
+ // and FilterStatus{} models from the database in a single transaction.
+ DeleteFilter(ctx context.Context, filter *gtsmodel.Filter) error
// GetFilterKeywordByID gets one filter keyword with the given ID.
GetFilterKeywordByID(ctx context.Context, id string) (*gtsmodel.FilterKeyword, error)
- // GetFilterKeywordsForFilterID gets filter keywords from the given filterID.
- GetFilterKeywordsForFilterID(ctx context.Context, filterID string) ([]*gtsmodel.FilterKeyword, error)
-
- // GetFilterKeywordsForAccountID gets filter keywords from the given accountID.
- GetFilterKeywordsForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.FilterKeyword, error)
+ // GetFilterKeywordsByIDs ...
+ GetFilterKeywordsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.FilterKeyword, error)
// PutFilterKeyword inserts a single filter keyword into the database.
PutFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) error
// UpdateFilterKeyword updates the given filter keyword.
- // Columns is optional, if not specified all will be updated.
- UpdateFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword, columns ...string) error
+ UpdateFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword, cols ...string) error
- // DeleteFilterKeywordByID deletes one filter keyword with the given id.
- DeleteFilterKeywordByID(ctx context.Context, id string) error
-
- //
-
- //
+ // DeleteFilterKeywordsByIDs deletes filter keywords with the given ids.
+ DeleteFilterKeywordsByIDs(ctx context.Context, ids ...string) error
// GetFilterStatusByID gets one filter status with the given ID.
GetFilterStatusByID(ctx context.Context, id string) (*gtsmodel.FilterStatus, error)
- // GetFilterStatusesForFilterID gets filter statuses from the given filterID.
- GetFilterStatusesForFilterID(ctx context.Context, filterID string) ([]*gtsmodel.FilterStatus, error)
-
- // GetFilterStatusesForAccountID gets filter keywords from the given accountID.
- GetFilterStatusesForAccountID(ctx context.Context, accountID string) ([]*gtsmodel.FilterStatus, error)
+ // GetFilterStatusesByIDs ...
+ GetFilterStatusesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.FilterStatus, error)
// PutFilterStatus inserts a single filter status into the database.
PutFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus) error
- // DeleteFilterStatusByID deletes one filter status with the given id.
- DeleteFilterStatusByID(ctx context.Context, id string) error
-
- //
+ // DeleteFilterStatusesByIDs deletes filter statuses with the given ids.
+ DeleteFilterStatusesByIDs(ctx context.Context, ids ...string) error
}
diff --git a/internal/filter/status/status.go b/internal/filter/status/status.go
index 7cf0a7a1e..1a611cdd1 100644
--- a/internal/filter/status/status.go
+++ b/internal/filter/status/status.go
@@ -24,22 +24,3 @@ import (
// ErrHideStatus indicates that a status has been filtered and should not be returned at all.
var ErrHideStatus = errors.New("hide status")
-
-// FilterContext determines the filters that apply to a given status or list of statuses.
-type FilterContext string
-
-const (
- // FilterContextNone means no filters should be applied.
- // There are no filters with this context; it's for internal use only.
- FilterContextNone FilterContext = ""
- // FilterContextHome means this status is being filtered as part of a home or list timeline.
- FilterContextHome FilterContext = "home"
- // FilterContextNotifications means this status is being filtered as part of the notifications timeline.
- FilterContextNotifications FilterContext = "notifications"
- // FilterContextPublic means this status is being filtered as part of a public or tag timeline.
- FilterContextPublic FilterContext = "public"
- // FilterContextThread means this status is being filtered as part of a thread's context.
- FilterContextThread FilterContext = "thread"
- // FilterContextAccount means this status is being filtered as part of an account's statuses.
- FilterContextAccount FilterContext = "account"
-)
diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go
index e2059474d..8f25d0754 100644
--- a/internal/gtserror/withcode.go
+++ b/internal/gtserror/withcode.go
@@ -118,7 +118,7 @@ func WrapWithCode(code int, err error) WithCode {
// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
func NewErrorBadRequest(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusBadRequest)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -131,7 +131,7 @@ func NewErrorBadRequest(original error, helpText ...string) WithCode {
// NewErrorUnauthorized returns an ErrorWithCode 401 with the given original error and optional help text.
func NewErrorUnauthorized(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusUnauthorized)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -144,7 +144,7 @@ func NewErrorUnauthorized(original error, helpText ...string) WithCode {
// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
func NewErrorForbidden(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusForbidden)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -157,7 +157,7 @@ func NewErrorForbidden(original error, helpText ...string) WithCode {
// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
func NewErrorNotFound(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusNotFound)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -170,7 +170,7 @@ func NewErrorNotFound(original error, helpText ...string) WithCode {
// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
func NewErrorInternalError(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusInternalServerError)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -183,7 +183,7 @@ func NewErrorInternalError(original error, helpText ...string) WithCode {
// NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text.
func NewErrorConflict(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusConflict)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -196,7 +196,7 @@ func NewErrorConflict(original error, helpText ...string) WithCode {
// NewErrorNotAcceptable returns an ErrorWithCode 406 with the given original error and optional help text.
func NewErrorNotAcceptable(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusNotAcceptable)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -209,7 +209,7 @@ func NewErrorNotAcceptable(original error, helpText ...string) WithCode {
// NewErrorUnprocessableEntity returns an ErrorWithCode 422 with the given original error and optional help text.
func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusUnprocessableEntity)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -222,7 +222,7 @@ func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode {
// NewErrorGone returns an ErrorWithCode 410 with the given original error and optional help text.
func NewErrorGone(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusGone)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
@@ -235,7 +235,7 @@ func NewErrorGone(original error, helpText ...string) WithCode {
// NewErrorNotImplemented returns an ErrorWithCode 501 with the given original error and optional help text.
func NewErrorNotImplemented(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusNotImplemented)
- if helpText != nil {
+ if len(helpText) > 0 {
safe = safe + ": " + strings.Join(helpText, ": ")
}
return &withCode{
diff --git a/internal/gtsmodel/common.go b/internal/gtsmodel/common.go
index e740bbb81..6fb415e5f 100644
--- a/internal/gtsmodel/common.go
+++ b/internal/gtsmodel/common.go
@@ -17,8 +17,17 @@
package gtsmodel
+// smallint is the largest size supported
+// by a PostgreSQL SMALLINT, since an SQLite
+// SMALLINT is actually variable in size.
+type smallint int16
+
// enumType is the type we (at least, should) use
-// for database enum types. it is the largest size
-// supported by a PostgreSQL SMALLINT, since an
-// SQLite SMALLINT is actually variable in size.
-type enumType int16
+// for database enum types, as smallest int size.
+type enumType smallint
+
+// bitFieldType is the type we use
+// for database int bit fields, at
+// least where the smallest int size
+// will suffice for number of fields.
+type bitFieldType smallint
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"
-)
diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go
index 329bcf30c..e6f0886f9 100644
--- a/internal/processing/account/bookmarks.go
+++ b/internal/processing/account/bookmarks.go
@@ -23,7 +23,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -75,7 +74,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode
}
// Convert the status.
- item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
+ item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Errorf(ctx, "error converting bookmarked status to api: %s", err)
continue
diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go
index 0ff9ef7e1..3b56750c5 100644
--- a/internal/processing/account/statuses.go
+++ b/internal/processing/account/statuses.go
@@ -24,7 +24,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -97,7 +96,7 @@ func (p *Processor) StatusesGet(
return nil, gtserror.NewErrorInternalError(err)
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID)
if err != nil {
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
return nil, gtserror.NewErrorInternalError(err)
@@ -105,7 +104,7 @@ func (p *Processor) StatusesGet(
for _, s := range filtered {
// Convert filtered statuses to API statuses.
- item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters)
+ item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, gtsmodel.FilterContextAccount, filters)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go
index 441a58384..83acddc84 100644
--- a/internal/processing/common/status.go
+++ b/internal/processing/common/status.go
@@ -213,7 +213,7 @@ func (p *Processor) GetAPIStatus(
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
target,
requester,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil,
)
if err != nil {
@@ -234,7 +234,7 @@ func (p *Processor) GetVisibleAPIStatuses(
ctx context.Context,
requester *gtsmodel.Account,
statuses []*gtsmodel.Status,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) []apimodel.Status {
@@ -277,7 +277,7 @@ func (p *Processor) GetVisibleAPIStatuses(
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
status,
requester,
- filterContext,
+ filterCtx,
filters,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go
index e31f60500..70fafa437 100644
--- a/internal/processing/conversations/conversations.go
+++ b/internal/processing/conversations/conversations.go
@@ -101,7 +101,7 @@ func (p *Processor) getFilters(
ctx context.Context,
requestingAccount *gtsmodel.Account,
) ([]*gtsmodel.Filter, gtserror.WithCode) {
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID)
if err != nil {
return nil, gtserror.NewErrorInternalError(
gtserror.Newf(
diff --git a/internal/processing/filters/common/common.go b/internal/processing/filters/common/common.go
new file mode 100644
index 000000000..a119d3bd4
--- /dev/null
+++ b/internal/processing/filters/common/common.go
@@ -0,0 +1,184 @@
+// 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 .
+
+package common
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+)
+
+type Processor struct{ state *state.State }
+
+func New(state *state.State) *Processor { return &Processor{state} }
+
+// CheckFilterExists calls .GetFilter() with a barebones context to not
+// fetch any sub-models, and not returning the result. this functionally
+// just uses .GetFilter() for the ownership and existence checks.
+func (p *Processor) CheckFilterExists(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) gtserror.WithCode {
+ _, errWithCode := p.GetFilter(gtscontext.SetBarebones(ctx), requester, id)
+ return errWithCode
+}
+
+// GetFilter fetches the filter with given ID, also checking
+// the given requesting account is the owner of the filter.
+func (p *Processor) GetFilter(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+ // Get the filter from the database with given ID.
+ filter, err := p.state.DB.GetFilterByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if filter == nil {
+ const text = "filter not found"
+ return nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Check that the requester owns it.
+ if filter.AccountID != requester.ID {
+ const text = "filter not found"
+ err := gtserror.New("filter does not belong to account")
+ return nil, gtserror.NewErrorNotFound(err, text)
+ }
+
+ return filter, nil
+}
+
+// GetFilterStatus fetches the filter status with given ID, also
+// checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterStatus(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterStatus,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter status from the database with given ID.
+ filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter status: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it even exists.
+ if filterStatus == nil {
+ const text = "filter status not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter status is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterStatus.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return filterStatus, filter, nil
+}
+
+// GetFilterKeyword fetches the filter keyword with given ID,
+// also checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterKeyword(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterKeyword,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter keyword from the database with given ID.
+ keyword, err := p.state.DB.GetFilterKeywordByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter keyword: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if keyword == nil {
+ const text = "filter keyword not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter keyword is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ keyword.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return keyword, filter, nil
+}
+
+// FromAPIContexts converts a slice of frontend API model FilterContext types to our internal FilterContexts bit field.
+func FromAPIContexts(apiContexts []apimodel.FilterContext) (gtsmodel.FilterContexts, gtserror.WithCode) {
+ var contexts gtsmodel.FilterContexts
+ for _, context := range apiContexts {
+ switch context {
+ case apimodel.FilterContextHome:
+ contexts.SetHome()
+ case apimodel.FilterContextNotifications:
+ contexts.SetNotifications()
+ case apimodel.FilterContextPublic:
+ contexts.SetPublic()
+ case apimodel.FilterContextThread:
+ contexts.SetThread()
+ case apimodel.FilterContextAccount:
+ contexts.SetAccount()
+ default:
+ text := fmt.Sprintf("unsupported filter context: %s", context)
+ return 0, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+ }
+ return contexts, nil
+}
diff --git a/internal/processing/filters/v1/convert.go b/internal/processing/filters/v1/convert.go
deleted file mode 100644
index 417cf7b7d..000000000
--- a/internal/processing/filters/v1/convert.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 .
-
-package v1
-
-import (
- "context"
- "fmt"
-
- apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
-)
-
-// apiFilter is a shortcut to return the API v1 filter version of the given
-// filter keyword, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (*apimodel.FilterV1, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterKeywordToAPIFilterV1(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter keyword to API v1 filter: %w", err))
- }
-
- return apiFilter, nil
-}
diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go
index 24517dd7b..b2ec69442 100644
--- a/internal/processing/filters/v1/create.go
+++ b/internal/processing/filters/v1/create.go
@@ -20,7 +20,7 @@ package v1
import (
"context"
"errors"
- "fmt"
+ "net/http"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -28,68 +28,72 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
"code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create a new filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new wrapping filter.
filter := >smodel.Filter{
ID: id.NewULID(),
- AccountID: account.ID,
+ AccountID: requester.ID,
Title: form.Phrase,
- Action: gtsmodel.FilterActionWarn,
}
+
if *form.Irreversible {
+ // Irreversible = action hide.
filter.Action = gtsmodel.FilterActionHide
- }
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
- }
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
+ } else {
+ // Default action = action warn.
+ filter.Action = gtsmodel.FilterActionWarn
}
- filterKeyword := >smodel.FilterKeyword{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- Filter: filter,
- Keyword: form.Phrase,
- WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
- }
- filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
-
- if err := p.state.DB.PutFilter(ctx, filter); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
- return nil, gtserror.NewErrorInternalError(err)
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
}
- apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(form.Context)
if errWithCode != nil {
return nil, errWithCode
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Create new keyword attached to filter.
+ filterKeyword := >smodel.FilterKeyword{
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ Keyword: form.Phrase,
+ WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
+ }
- return apiFilter, nil
+ // Attach the new keyword to filter before insert.
+ filter.Keywords = append(filter.Keywords, filterKeyword)
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+
+ // Insert newly created filter into the database.
+ switch err := p.state.DB.PutFilter(ctx, filter); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
+
+ // Return as converted frontend filter keyword model.
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
diff --git a/internal/processing/filters/v1/delete.go b/internal/processing/filters/v1/delete.go
index 6a081ff04..cab8b185d 100644
--- a/internal/processing/filters/v1/delete.go
+++ b/internal/processing/filters/v1/delete.go
@@ -19,52 +19,52 @@ package v1
import (
"context"
- "errors"
+ "slices"
- "code.superseriousbusiness.org/gotosocial/internal/db"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
-// Delete an existing filter keyword and (if empty afterwards) filter for the given account.
+// Delete an existing filter keyword and (if empty
+// afterwards) filter for the given account.
func (p *Processor) Delete(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
) gtserror.WithCode {
- // Get enough of the filter keyword that we can look up its filter ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return gtserror.NewErrorNotFound(err)
- }
- return gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return gtserror.NewErrorNotFound(nil)
- }
-
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get the filter keyword with given ID, and associated filter, also checking ownership.
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return errWithCode
}
if len(filter.Keywords) > 1 || len(filter.Statuses) > 0 {
- // The filter has other keywords or statuses. Delete only the requested filter keyword.
- if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
+ // The filter has other keywords or statuses, just delete the one filter keyword.
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeyword.ID); err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool {
+ return filterKeyword.ID == id
+ })
+
+ // Update filter in the database now the keyword has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
} else {
- // Delete the entire filter.
- if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil {
+ // Delete the filter and this keyword that is attached to it.
+ if err := p.state.DB.DeleteFilter(ctx, filter); err != nil {
+ err := gtserror.Newf("error deleting filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v1/filters.go b/internal/processing/filters/v1/filters.go
index 89b509912..bcbbd70c0 100644
--- a/internal/processing/filters/v1/filters.go
+++ b/internal/processing/filters/v1/filters.go
@@ -18,19 +18,25 @@
package v1
import (
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
"code.superseriousbusiness.org/gotosocial/internal/processing/stream"
"code.superseriousbusiness.org/gotosocial/internal/state"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
+ // embedded common logic
+ c *common.Processor
+
state *state.State
converter *typeutils.Converter
stream *stream.Processor
}
-func New(state *state.State, converter *typeutils.Converter, stream *stream.Processor) Processor {
+func New(state *state.State, converter *typeutils.Converter, common *common.Processor, stream *stream.Processor) Processor {
return Processor{
+ c: common,
+
state: state,
converter: converter,
stream: stream,
diff --git a/internal/processing/filters/v1/get.go b/internal/processing/filters/v1/get.go
index ad35e6272..bdde123e9 100644
--- a/internal/processing/filters/v1/get.go
+++ b/internal/processing/filters/v1/get.go
@@ -25,47 +25,58 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Get looks up a filter keyword by ID and returns it as a v1 filter.
-func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, gtserror.WithCode) {
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
+func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, gtserror.WithCode) {
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
- }
-
- return p.apiFilter(ctx, filterKeyword)
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
// GetAll looks up all filter keywords for the current account and returns them as v1 filters.
-func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) {
- filters, err := p.state.DB.GetFilterKeywordsForAccountID(
- ctx,
- account.ID,
+func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) {
+ var totalKeywords int
+
+ // Get a list of all filters owned by this account,
+ // (without any sub-models attached, done later).
+ filters, err := p.state.DB.GetFiltersByAccountID(
+ gtscontext.SetBarebones(ctx),
+ requester.ID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filters: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters := make([]*apimodel.FilterV1, 0, len(filters))
+ // Get a total count of all expected
+ // keywords for slice preallocation.
for _, filter := range filters {
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
- if errWithCode != nil {
- return nil, errWithCode
+ totalKeywords += len(filter.KeywordIDs)
+ }
+
+ // Create a slice to store converted V1 frontend models.
+ apiFilters := make([]*apimodel.FilterV1, 0, totalKeywords)
+
+ for _, filter := range filters {
+ // For each of the fetched filters, fetch all of their associated keywords.
+ keywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs)
+ if err != nil {
+ err := gtserror.Newf("error getting filter keywords: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters = append(apiFilters, apiFilter)
+ // Convert each keyword to frontend.
+ for _, keyword := range keywords {
+ apiFilter := typeutils.FilterKeywordToAPIFilterV1(filter, keyword)
+ apiFilters = append(apiFilters, apiFilter)
+ }
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go
index 8b50c3fcf..7e25e6fde 100644
--- a/internal/processing/filters/v1/update.go
+++ b/internal/processing/filters/v1/update.go
@@ -21,77 +21,59 @@ import (
"context"
"errors"
"fmt"
+ "net/http"
"strings"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
- "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Update an existing filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Update(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
form *apimodel.FilterCreateUpdateRequestV1,
) (*apimodel.FilterV1, gtserror.WithCode) {
- // Get enough of the filter keyword that we can look up its filter ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
+ // Get the filter keyword with given ID, and associated filter, also checking ownership.
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
+ var title string
+ var action gtsmodel.FilterAction
+ var contexts gtsmodel.FilterContexts
+ var expiresAt time.Time
+ var wholeword bool
+
+ // Get filter title.
+ title = form.Phrase
- title := form.Phrase
- action := gtsmodel.FilterActionWarn
if *form.Irreversible {
+ // Irreversible = action hide.
action = gtsmodel.FilterActionHide
+ } else {
+ // Default action = action warn.
+ action = gtsmodel.FilterActionWarn
}
- expiresAt := time.Time{}
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- expiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
+
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ expiresAt = time.Now().Add(expiresIn)
}
- contextHome := false
- contextNotifications := false
- contextPublic := false
- contextThread := false
- contextAccount := false
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- contextHome = true
- case apimodel.FilterContextNotifications:
- contextNotifications = true
- case apimodel.FilterContextPublic:
- contextPublic = true
- case apimodel.FilterContextThread:
- contextThread = true
- case apimodel.FilterContextAccount:
- contextAccount = true
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
+
+ // Parse contexts filter applies in from incoming form data.
+ contexts, errWithCode = common.FromAPIContexts(form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
}
// v1 filter APIs can't change certain fields for a filter with multiple keywords or any statuses,
@@ -108,11 +90,7 @@ func (p *Processor) Update(
if expiresAt != filter.ExpiresAt {
forbiddenFields = append(forbiddenFields, "expires_in")
}
- if contextHome != util.PtrOrValue(filter.ContextHome, false) ||
- contextNotifications != util.PtrOrValue(filter.ContextNotifications, false) ||
- contextPublic != util.PtrOrValue(filter.ContextPublic, false) ||
- contextThread != util.PtrOrValue(filter.ContextThread, false) ||
- contextAccount != util.PtrOrValue(filter.ContextAccount, false) {
+ if contexts != filter.Contexts {
forbiddenFields = append(forbiddenFields, "context")
}
if len(forbiddenFields) > 0 {
@@ -122,54 +100,75 @@ func (p *Processor) Update(
}
}
- // Now that we've checked that the changes are legal, apply them to the filter and keyword.
- filter.Title = title
- filter.Action = action
- filter.ExpiresAt = expiresAt
- filter.ContextHome = &contextHome
- filter.ContextNotifications = &contextNotifications
- filter.ContextPublic = &contextPublic
- filter.ContextThread = &contextThread
- filter.ContextAccount = &contextAccount
- filterKeyword.Keyword = form.Phrase
- filterKeyword.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))
+ // Filter columns that
+ // we're going to update.
+ var filterCols []string
+ var keywordCols []string
- // We only want to update the relevant filter keyword.
- filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
- filter.Statuses = nil
- filterKeyword.Filter = filter
+ // Check for changed filter title / filter keyword phrase.
+ if title != filter.Title || title != filterKeyword.Keyword {
+ keywordCols = append(keywordCols, "keyword")
+ filterCols = append(filterCols, "title")
+ filterKeyword.Keyword = title
+ filter.Title = title
+ }
- filterColumns := []string{
- "title",
- "action",
- "expires_at",
- "context_home",
- "context_notifications",
- "context_public",
- "context_thread",
- "context_account",
+ // Check for changed action.
+ if action != filter.Action {
+ filterCols = append(filterCols, "action")
+ filter.Action = action
}
- filterKeywordColumns := [][]string{
- {
- "keyword",
- "whole_word",
- },
+
+ // Check for changed filter expiry time.
+ if !expiresAt.Equal(filter.ExpiresAt) {
+ filterCols = append(filterCols, "expires_at")
+ filter.ExpiresAt = expiresAt
}
- if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, nil, nil); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+
+ // Check for changed filter context.
+ if contexts != filter.Contexts {
+ filterCols = append(filterCols, "contexts")
+ filter.Contexts = contexts
+ }
+
+ // Check for changed wholeword flag.
+ if form.WholeWord != nil &&
+ *form.WholeWord != *filterKeyword.WholeWord {
+ keywordCols = append(keywordCols, "whole_word")
+ filterKeyword.WholeWord = &wholeword
+ }
+
+ // Update filter keyword model in the database with determined changed cols.
+ switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, keywordCols...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
- if errWithCode != nil {
- return nil, errWithCode
+ // Update filter model in the database with determined changed cols.
+ switch err := p.state.DB.UpdateFilter(ctx, filter, filterCols...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
- return apiFilter, nil
+ // Return as converted frontend filter keyword model.
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/convert.go b/internal/processing/filters/v2/convert.go
deleted file mode 100644
index 590edd04b..000000000
--- a/internal/processing/filters/v2/convert.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 .
-
-package v2
-
-import (
- "context"
- "fmt"
-
- apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
-)
-
-// apiFilter is a shortcut to return the API v2 filter version of the given
-// filter, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err))
- }
-
- return apiFilter, nil
-}
diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go
index c221e1539..d77c23424 100644
--- a/internal/processing/filters/v2/create.go
+++ b/internal/processing/filters/v2/create.go
@@ -20,7 +20,7 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -28,79 +28,85 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
- "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create a new filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new filter model.
filter := >smodel.Filter{
ID: id.NewULID(),
AccountID: account.ID,
Title: form.Title,
- Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction),
- }
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
- }
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
}
- for _, formKeyword := range form.Keywords {
- filterKeyword := >smodel.FilterKeyword{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- Filter: filter,
- Keyword: formKeyword.Keyword,
- WholeWord: formKeyword.WholeWord,
- }
- filter.Keywords = append(filter.Keywords, filterKeyword)
+ // Parse filter action from form and set on filter, checking for validity.
+ filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
+ if filter.Action == 0 {
+ const text = "invalid filter action"
+ return nil, gtserror.NewWithCode(http.StatusBadRequest, text)
}
- for _, formStatus := range form.Statuses {
- filterStatus := >smodel.FilterStatus{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- Filter: filter,
- StatusID: formStatus.StatusID,
- }
- filter.Statuses = append(filter.Statuses, filterStatus)
- }
-
- if err := p.state.DB.PutFilter(ctx, filter); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate title, keyword, or status")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(form.Context)
if errWithCode != nil {
return nil, errWithCode
}
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
+ }
+
+ // Create new attached filter keywords.
+ for _, keyword := range form.Keywords {
+ filterKeyword := >smodel.FilterKeyword{
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ Keyword: keyword.Keyword,
+ WholeWord: keyword.WholeWord,
+ }
+
+ // Append the new filter key word to filter itself.
+ filter.Keywords = append(filter.Keywords, filterKeyword)
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+ }
+
+ // Create new attached filter statuses.
+ for _, status := range form.Statuses {
+ filterStatus := >smodel.FilterStatus{
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: status.StatusID,
+ }
+
+ // Append the new filter status to filter itself.
+ filter.Statuses = append(filter.Statuses, filterStatus)
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+ }
+
+ // Insert the new filter model into the database.
+ switch err := p.state.DB.PutFilter(ctx, filter); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title, keyword or status"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
- return apiFilter, nil
+ // Return as converted frontend filter model.
+ return typeutils.FilterToAPIFilterV2(filter), nil
}
diff --git a/internal/processing/filters/v2/delete.go b/internal/processing/filters/v2/delete.go
index b6a4c6321..ca3ade431 100644
--- a/internal/processing/filters/v2/delete.go
+++ b/internal/processing/filters/v2/delete.go
@@ -19,38 +19,33 @@ package v2
import (
"context"
- "fmt"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
-// Delete an existing filter and all its attached keywords and statuses for the given account.
+// Delete an existing filter and all its attached
+// keywords and statuses for the given account.
func (p *Processor) Delete(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterID string,
) gtserror.WithCode {
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Check that the account owns it.
- if filter.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
- }
-
- // Delete the entire filter.
- if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil {
+ // Delete filter from the database with all associated models.
+ if err := p.state.DB.DeleteFilter(ctx, filter); err != nil {
+ err := gtserror.Newf("error deleting filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/filters.go b/internal/processing/filters/v2/filters.go
index 82fef36b6..8c0ade1ca 100644
--- a/internal/processing/filters/v2/filters.go
+++ b/internal/processing/filters/v2/filters.go
@@ -18,19 +18,25 @@
package v2
import (
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
"code.superseriousbusiness.org/gotosocial/internal/processing/stream"
"code.superseriousbusiness.org/gotosocial/internal/state"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
+ // embedded common logic
+ c *common.Processor
+
state *state.State
converter *typeutils.Converter
stream *stream.Processor
}
-func New(state *state.State, converter *typeutils.Converter, stream *stream.Processor) Processor {
+func New(state *state.State, converter *typeutils.Converter, common *common.Processor, stream *stream.Processor) Processor {
return Processor{
+ c: common,
+
state: state,
converter: converter,
stream: stream,
diff --git a/internal/processing/filters/v2/get.go b/internal/processing/filters/v2/get.go
index 7240d1ba3..4cdf9e8ee 100644
--- a/internal/processing/filters/v2/get.go
+++ b/internal/processing/filters/v2/get.go
@@ -19,56 +19,43 @@ package v2
import (
"context"
- "errors"
- "fmt"
"slices"
"strings"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- "code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Get looks up a filter by ID and returns it with keywords and statuses.
-func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) {
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
+func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) {
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
- }
-
- return p.apiFilter(ctx, filter)
+ return typeutils.FilterToAPIFilterV2(filter), nil
}
// GetAll looks up all filters for the current account and returns them with keywords and statuses.
-func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) {
- filters, err := p.state.DB.GetFiltersForAccountID(
- ctx,
- account.ID,
- )
+func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) {
+
+ // Get all filters belonging to this requester from the database.
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID)
if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ err := gtserror.Newf("error getting account filters: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters := make([]*apimodel.FilterV2, 0, len(filters))
- for _, filter := range filters {
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- apiFilters = append(apiFilters, apiFilter)
+ // Convert all these filters to frontend API models.
+ apiFilters := make([]*apimodel.FilterV2, len(filters))
+ if len(apiFilters) != len(filters) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filter := range filters {
+ apiFilter := typeutils.FilterToAPIFilterV2(filter)
+ apiFilters[i] = apiFilter
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/keywordcreate.go b/internal/processing/filters/v2/keywordcreate.go
index 89ada34f4..da91d5fd3 100644
--- a/internal/processing/filters/v2/keywordcreate.go
+++ b/internal/processing/filters/v2/keywordcreate.go
@@ -20,51 +20,60 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// KeywordCreate adds a filter keyword to an existing filter for the given account, using the provided parameters.
// These params should have already been normalized and validated by the time they reach this function.
-func (p *Processor) KeywordCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+func (p *Processor) KeywordCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) {
+
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Create new filter keyword model.
filterKeyword := >smodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: account.ID,
FilterID: filter.ID,
Keyword: form.Keyword,
WholeWord: form.WholeWord,
}
- if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate keyword")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Insert the new filter keyword model into the database.
+ switch err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Now update the filter it is attached to with new keyword.
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+ filter.Keywords = append(filter.Keywords, filterKeyword)
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ // Update the existing filter model in the database (only the needed col).
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
+
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/keyworddelete.go b/internal/processing/filters/v2/keyworddelete.go
index 390c7b2cf..a0ec887e3 100644
--- a/internal/processing/filters/v2/keyworddelete.go
+++ b/internal/processing/filters/v2/keyworddelete.go
@@ -19,7 +19,7 @@ package v2
import (
"context"
- "fmt"
+ "slices"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -28,29 +28,34 @@ import (
// KeywordDelete deletes an existing filter keyword from a filter.
func (p *Processor) KeywordDelete(
ctx context.Context,
- account *gtsmodel.Account,
- filterID string,
+ requester *gtsmodel.Account,
+ filterKeywordID string,
) gtserror.WithCode {
- // Get the filter keyword.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get filter keyword with given ID, also checking ownership to requester.
+ _, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Check that the account owns it.
- if filterKeyword.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
- }
-
- // Delete the filter keyword.
- if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
+ // Delete this one filter keyword from the database, now ownership is confirmed.
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeywordID); err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool {
+ return filterKeywordID == id
+ })
+
+ // Update filter in the database now the keyword has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/keywordget.go b/internal/processing/filters/v2/keywordget.go
index e824b2e57..3cf120ed8 100644
--- a/internal/processing/filters/v2/keywordget.go
+++ b/internal/processing/filters/v2/keywordget.go
@@ -20,7 +20,6 @@ package v2
import (
"context"
"errors"
- "fmt"
"slices"
"strings"
@@ -29,54 +28,47 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// KeywordGet looks up a filter keyword by ID.
-func (p *Processor) KeywordGet(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) {
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
+func (p *Processor) KeywordGet(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) {
+ filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
- }
-
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
// KeywordsGetForFilterID looks up all filter keywords for the given filter.
-func (p *Processor) KeywordsGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
- }
+func (p *Processor) KeywordsGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) {
- filterKeywords, err := p.state.DB.GetFilterKeywordsForFilterID(
- ctx,
- filter.ID,
+ // Get the filter with given ID (but
+ // without any sub-models attached).
+ filter, errWithCode := p.c.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Fetch all associated filter keywords to the determined existent filter.
+ filterKeywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter keywords: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilterKeywords := make([]*apimodel.FilterKeyword, 0, len(filterKeywords))
- for _, filterKeyword := range filterKeywords {
- apiFilterKeywords = append(apiFilterKeywords, p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
+ // Convert all of the filter keyword models from internal to frontend form.
+ apiFilterKeywords := make([]*apimodel.FilterKeyword, len(filterKeywords))
+ if len(apiFilterKeywords) != len(filterKeywords) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filterKeyword := range filterKeywords {
+ apiFilterKeywords[i] = typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword)
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/keywordupdate.go b/internal/processing/filters/v2/keywordupdate.go
index 4c0a54b83..9d1e5bd0c 100644
--- a/internal/processing/filters/v2/keywordupdate.go
+++ b/internal/processing/filters/v2/keywordupdate.go
@@ -20,50 +20,51 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// KeywordUpdate updates an existing filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) KeywordUpdate(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
form *apimodel.FilterKeywordCreateUpdateRequest,
) (*apimodel.FilterKeyword, gtserror.WithCode) {
- // Get the filter keyword by ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
+
+ // Get the filter keyword with given ID, also checking ownership to requester.
+ filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Update the keyword model fields.
filterKeyword.Keyword = form.Keyword
filterKeyword.WholeWord = form.WholeWord
- if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, "keyword", "whole_word"); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate keyword")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Update existing filter keyword model in the database, (only necessary cols).
+ switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, []string{
+ "keyword", "whole_word"}...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/statuscreate.go b/internal/processing/filters/v2/statuscreate.go
index 927986c69..1acab448c 100644
--- a/internal/processing/filters/v2/statuscreate.go
+++ b/internal/processing/filters/v2/statuscreate.go
@@ -20,50 +20,59 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// StatusCreate adds a filter status to an existing filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) StatusCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+func (p *Processor) StatusCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) {
+
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Create new filter status model.
filterStatus := >smodel.FilterStatus{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- StatusID: form.StatusID,
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: form.StatusID,
}
- if err := p.state.DB.PutFilterStatus(ctx, filterStatus); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate status")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Insert the new filter status model into the database.
+ switch err := p.state.DB.PutFilterStatus(ctx, filterStatus); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate status"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter status: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Now update the filter it is attached to with new status.
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+ filter.Statuses = append(filter.Statuses, filterStatus)
- return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
+ // Update the existing filter model in the database (only the needed col).
+ if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
+
+ return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil
}
diff --git a/internal/processing/filters/v2/statusdelete.go b/internal/processing/filters/v2/statusdelete.go
index e25f7279e..4309bac1a 100644
--- a/internal/processing/filters/v2/statusdelete.go
+++ b/internal/processing/filters/v2/statusdelete.go
@@ -19,7 +19,7 @@ package v2
import (
"context"
- "fmt"
+ "slices"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -28,29 +28,34 @@ import (
// StatusDelete deletes an existing filter status from a filter.
func (p *Processor) StatusDelete(
ctx context.Context,
- account *gtsmodel.Account,
- filterID string,
+ requester *gtsmodel.Account,
+ filterStatusID string,
) gtserror.WithCode {
- // Get the filter status.
- filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get filter status with given ID, also checking ownership to requester.
+ _, filter, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Check that the account owns it.
- if filterStatus.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
- )
- }
-
- // Delete the filter status.
- if err := p.state.DB.DeleteFilterStatusByID(ctx, filterStatus.ID); err != nil {
+ // Delete this one filter status from the database, now ownership is confirmed.
+ if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, filterStatusID); err != nil {
+ err := gtserror.Newf("error deleting filter status: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.StatusIDs = slices.DeleteFunc(filter.StatusIDs, func(id string) bool {
+ return filterStatusID == id
+ })
+
+ // Update filter in the database now the status has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/statusget.go b/internal/processing/filters/v2/statusget.go
index 06a56d271..7aa51f830 100644
--- a/internal/processing/filters/v2/statusget.go
+++ b/internal/processing/filters/v2/statusget.go
@@ -20,7 +20,6 @@ package v2
import (
"context"
"errors"
- "fmt"
"slices"
"strings"
@@ -29,54 +28,47 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// StatusGet looks up a filter status by ID.
-func (p *Processor) StatusGet(ctx context.Context, account *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) {
- filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterStatusID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
+func (p *Processor) StatusGet(ctx context.Context, requester *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) {
+ filterStatus, _, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- if filterStatus.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
- )
- }
-
- return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
+ return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil
}
// StatusesGetForFilterID looks up all filter statuses for the given filter.
-func (p *Processor) StatusesGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
- }
+func (p *Processor) StatusesGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) {
- filterStatuses, err := p.state.DB.GetFilterStatusesForFilterID(
- ctx,
- filter.ID,
+ // Get the filter with given ID (but
+ // without any sub-models attached).
+ filter, errWithCode := p.c.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Fetch all associated filter statuses to the determined existent filter.
+ filterStatuses, err := p.state.DB.GetFilterStatusesByIDs(ctx, filter.StatusIDs)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilterStatuses := make([]*apimodel.FilterStatus, 0, len(filterStatuses))
- for _, filterStatus := range filterStatuses {
- apiFilterStatuses = append(apiFilterStatuses, p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus))
+ // Convert all of the filter status models from internal to frontend form.
+ apiFilterStatuses := make([]*apimodel.FilterStatus, len(filterStatuses))
+ if len(apiFilterStatuses) != len(filterStatuses) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filterStatus := range filterStatuses {
+ apiFilterStatuses[i] = typeutils.FilterStatusToAPIFilterStatus(filterStatus)
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go
index 9d38cac66..96a43612f 100644
--- a/internal/processing/filters/v2/update.go
+++ b/internal/processing/filters/v2/update.go
@@ -20,7 +20,8 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
+ "slices"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -28,243 +29,356 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
- "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Update an existing filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Update(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterID string,
form *apimodel.FilterUpdateRequestV2,
) (*apimodel.FilterV2, gtserror.WithCode) {
- var errWithCode gtserror.WithCode
-
- // Get the filter by ID, with existing keywords and statuses.
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
- }
-
- // Filter columns that we're going to update.
- filterColumns := []string{}
-
- // Apply filter changes.
- if form.Title != nil {
- filterColumns = append(filterColumns, "title")
- filter.Title = *form.Title
- }
- if form.FilterAction != nil {
- filterColumns = append(filterColumns, "action")
- filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
- }
- if form.ExpiresIn != nil {
- expiresIn := *form.ExpiresIn
- filterColumns = append(filterColumns, "expires_at")
- if expiresIn == 0 {
- // Unset the expiration date.
- filter.ExpiresAt = time.Time{}
- } else {
- // Update the expiration date.
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(expiresIn))
- }
- }
- if form.Context != nil {
- filterColumns = append(filterColumns,
- "context_home",
- "context_notifications",
- "context_public",
- "context_thread",
- "context_account",
- )
- filter.ContextHome = util.Ptr(false)
- filter.ContextNotifications = util.Ptr(false)
- filter.ContextPublic = util.Ptr(false)
- filter.ContextThread = util.Ptr(false)
- filter.ContextAccount = util.Ptr(false)
- for _, context := range *form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
- }
- }
-
- filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords)
- if err != nil {
- return nil, errWithCode
- }
-
- deleteFilterStatusIDs, errWithCode := applyStatusChanges(filter, form.Statuses)
- if err != nil {
- return nil, errWithCode
- }
-
- if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, deleteFilterKeywordIDs, deleteFilterStatusIDs); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
if errWithCode != nil {
return nil, errWithCode
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Filter columns that
+ // we're going to update.
+ cols := make([]string, 0, 6)
- return apiFilter, nil
+ // Check for title change.
+ if form.Title != nil {
+ cols = append(cols, "title")
+ filter.Title = *form.Title
+ }
+
+ // Check action type change.
+ if form.FilterAction != nil {
+ cols = append(cols, "action")
+
+ // Parse filter action from form and set on filter, checking for validity.
+ filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
+ if filter.Action == 0 {
+ const text = "invalid filter action"
+ return nil, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+ }
+
+ // Check expiry change.
+ if form.ExpiresIn != nil {
+ cols = append(cols, "expires_at")
+ filter.ExpiresAt = time.Time{}
+
+ // Check form for valid
+ // expiry and set on filter.
+ if *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
+ }
+ }
+
+ // Check context change.
+ if form.Context != nil {
+ cols = append(cols, "contexts")
+
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(*form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+ }
+
+ // Check for any changes to attached keywords on filter.
+ keywordQs, errWithCode := p.updateFilterKeywords(ctx,
+ filter, form.Keywords)
+ if errWithCode != nil {
+ return nil, errWithCode
+ } else if len(keywordQs.create) > 0 || len(keywordQs.delete) > 0 {
+
+ // Attached keywords have changed.
+ cols = append(cols, "keywords")
+ }
+
+ // Check for any changes to attached statuses on filter.
+ statusQs, errWithCode := p.updateFilterStatuses(ctx,
+ filter, form.Statuses)
+ if errWithCode != nil {
+ return nil, errWithCode
+ } else if len(statusQs.create) > 0 || len(statusQs.delete) > 0 {
+
+ // Attached statuses have changed.
+ cols = append(cols, "statuses")
+ }
+
+ // Perform all the deferred database queries.
+ errWithCode = performTxs(keywordQs, statusQs)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Update the filter model in the database with determined cols.
+ switch err := p.state.DB.UpdateFilter(ctx, filter, cols...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Stream a filters changed event to WS.
+ p.stream.FiltersChanged(ctx, requester)
+
+ // Return as converted frontend filter model.
+ return typeutils.FilterToAPIFilterV2(filter), nil
}
-// applyKeywordChanges applies the provided changes to the filter's keywords in place,
-// and returns a list of lists of filter columns to update, and a list of filter keyword IDs to delete.
-func applyKeywordChanges(filter *gtsmodel.Filter, formKeywords []apimodel.FilterKeywordCreateUpdateDeleteRequest) ([][]string, []string, gtserror.WithCode) {
- if len(formKeywords) == 0 {
- // Detach currently existing keywords from the filter so we don't change them.
- filter.Keywords = nil
- return nil, nil, nil
+func (p *Processor) updateFilterKeywords(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterKeywordCreateUpdateDeleteRequest) (deferredQs, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No keyword changes.
+ return deferredQs{}, nil
}
- deleteFilterKeywordIDs := []string{}
- filterKeywordsByID := map[string]*gtsmodel.FilterKeyword{}
- filterKeywordColumnsByID := map[string][]string{}
- for _, filterKeyword := range filter.Keywords {
- filterKeywordsByID[filterKeyword.ID] = filterKeyword
- }
-
- for _, formKeyword := range formKeywords {
- if formKeyword.ID != nil {
- id := *formKeyword.ID
- filterKeyword, ok := filterKeywordsByID[id]
- if !ok {
- return nil, nil, gtserror.NewErrorNotFound(
- fmt.Errorf("couldn't find filter keyword '%s' to update or delete", id),
- )
+ var deferred deferredQs
+ for _, request := range form {
+ if request.ID != nil {
+ // Look by ID for keyword attached to filter.
+ idx := slices.IndexFunc(filter.Keywords,
+ func(f *gtsmodel.FilterKeyword) bool {
+ return f.ID == (*request.ID)
+ })
+ if idx == -1 {
+ const text = "filter keyword not found"
+ return deferred, gtserror.NewWithCode(http.StatusNotFound, text)
}
- // Process deletes.
- if *formKeyword.Destroy {
- delete(filterKeywordsByID, id)
- deleteFilterKeywordIDs = append(deleteFilterKeywordIDs, id)
+ // If this is a delete, update filter's id list.
+ if request.Destroy != nil && *request.Destroy {
+ filter.Keywords = slices.Delete(filter.Keywords, idx, idx+1)
+ filter.KeywordIDs = slices.Delete(filter.KeywordIDs, idx, idx+1)
+
+ // Append database delete to funcs for later processing by caller.
+ deferred.delete = append(deferred.delete, func() gtserror.WithCode {
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, *request.ID); //
+ err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
continue
}
- // Process updates.
- columns := make([]string, 0, 2)
- if formKeyword.Keyword != nil {
- columns = append(columns, "keyword")
- filterKeyword.Keyword = *formKeyword.Keyword
+ // Get the filter keyword at index.
+ filterKeyword := filter.Keywords[idx]
+
+ // Filter keywords database
+ // columns we need to update.
+ cols := make([]string, 0, 2)
+
+ // Check for changes to keyword string.
+ if val := request.Keyword; val != nil {
+ cols = append(cols, "keyword")
+ filterKeyword.Keyword = *val
}
- if formKeyword.WholeWord != nil {
- columns = append(columns, "whole_word")
- filterKeyword.WholeWord = formKeyword.WholeWord
+
+ // Check for changes to wholeword flag.
+ if val := request.WholeWord; val != nil {
+ cols = append(cols, "whole_word")
+ filterKeyword.WholeWord = val
}
- filterKeywordColumnsByID[id] = columns
+
+ // Verify that this is valid regular expression.
+ if err := filterKeyword.Compile(); err != nil {
+ const text = "invalid regular expression"
+ err := gtserror.Newf("invalid regular expression: %w", err)
+ return deferred, gtserror.NewWithCodeSafe(
+ http.StatusBadRequest,
+ err, text,
+ )
+ }
+
+ if len(cols) > 0 {
+ // Append database update to funcs for later processing by caller.
+ deferred.update = append(deferred.update, func() gtserror.WithCode {
+ if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, cols...); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate keyword"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error updating filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
+ }
+
continue
}
- // Process creates.
+ // Check for valid request.
+ if request.Keyword == nil {
+ const text = "missing keyword"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+
+ // Create new filter keyword for insert.
filterKeyword := >smodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: filter.AccountID,
FilterID: filter.ID,
- Filter: filter,
- Keyword: *formKeyword.Keyword,
- WholeWord: util.Ptr(util.PtrOrValue(formKeyword.WholeWord, false)),
+ Keyword: *request.Keyword,
+ WholeWord: request.WholeWord,
}
- filterKeywordsByID[filterKeyword.ID] = filterKeyword
- // Don't need to set columns, as we're using all of them.
- }
- // Replace the filter's keywords list with our updated version.
- filterKeywordColumns := [][]string{}
- filter.Keywords = nil
- for id, filterKeyword := range filterKeywordsByID {
+ // Verify that this is valid regular expression.
+ if err := filterKeyword.Compile(); err != nil {
+ const text = "invalid regular expression"
+ err := gtserror.Newf("invalid regular expression: %w", err)
+ return deferred, gtserror.NewWithCodeSafe(
+ http.StatusBadRequest,
+ err, text,
+ )
+ }
+
+ // Append new filter keyword to filter and list of IDs.
filter.Keywords = append(filter.Keywords, filterKeyword)
- // Okay to use the nil slice zero value for entries being created instead of updated.
- filterKeywordColumns = append(filterKeywordColumns, filterKeywordColumnsByID[id])
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred.create = append(deferred.create, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate keyword"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
}
- return filterKeywordColumns, deleteFilterKeywordIDs, nil
+ return deferred, nil
}
-// applyKeywordChanges applies the provided changes to the filter's keywords in place,
-// and returns a list of filter status IDs to delete.
-func applyStatusChanges(filter *gtsmodel.Filter, formStatuses []apimodel.FilterStatusCreateDeleteRequest) ([]string, gtserror.WithCode) {
- if len(formStatuses) == 0 {
- // Detach currently existing statuses from the filter so we don't change them.
- filter.Statuses = nil
- return nil, nil
+func (p *Processor) updateFilterStatuses(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterStatusCreateDeleteRequest) (deferredQs, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No keyword changes.
+ return deferredQs{}, nil
}
- deleteFilterStatusIDs := []string{}
- filterStatusesByID := map[string]*gtsmodel.FilterStatus{}
- for _, filterStatus := range filter.Statuses {
- filterStatusesByID[filterStatus.ID] = filterStatus
- }
-
- for _, formStatus := range formStatuses {
- if formStatus.ID != nil {
- id := *formStatus.ID
- _, ok := filterStatusesByID[id]
- if !ok {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("couldn't find filter status '%s' to delete", id),
- )
+ var deferred deferredQs
+ for _, request := range form {
+ if request.ID != nil {
+ // Look by ID for status attached to filter.
+ idx := slices.IndexFunc(filter.Statuses,
+ func(f *gtsmodel.FilterStatus) bool {
+ return f.ID == *request.ID
+ })
+ if idx == -1 {
+ const text = "filter status not found"
+ return deferred, gtserror.NewWithCode(http.StatusNotFound, text)
}
- // Process deletes.
- if *formStatus.Destroy {
- delete(filterStatusesByID, id)
- deleteFilterStatusIDs = append(deleteFilterStatusIDs, id)
- continue
- }
+ // If this is a delete, update filter's id list.
+ if request.Destroy != nil && *request.Destroy {
+ filter.Statuses = slices.Delete(filter.Statuses, idx, idx+1)
+ filter.StatusIDs = slices.Delete(filter.StatusIDs, idx, idx+1)
- // Filter statuses don't have updates.
+ // Append database delete to funcs for later processing by caller.
+ deferred.delete = append(deferred.delete, func() gtserror.WithCode {
+ if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, *request.ID); //
+ err != nil {
+ err := gtserror.Newf("error deleting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
+ }
continue
}
- // Process creates.
- filterStatus := >smodel.FilterStatus{
- ID: id.NewULID(),
- AccountID: filter.AccountID,
- FilterID: filter.ID,
- Filter: filter,
- StatusID: *formStatus.StatusID,
+ // Check for valid request.
+ if request.StatusID == nil {
+ const text = "missing status"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
}
- filterStatusesByID[filterStatus.ID] = filterStatus
- }
- // Replace the filter's keywords list with our updated version.
- filter.Statuses = nil
- for _, filterStatus := range filterStatusesByID {
+ // Create new filter status for insert.
+ filterStatus := >smodel.FilterStatus{
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: *request.StatusID,
+ }
+
+ // Append new filter status to filter and list of IDs.
filter.Statuses = append(filter.Statuses, filterStatus)
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred.create = append(deferred.create, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterStatus(ctx, filterStatus); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate status"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
}
- return deleteFilterStatusIDs, nil
+ return deferred, nil
+}
+
+// deferredQs stores selection of
+// deferred database queries.
+type deferredQs struct {
+ create []func() gtserror.WithCode
+ update []func() gtserror.WithCode
+ delete []func() gtserror.WithCode
+}
+
+// performTx performs the passed deferredQs functions,
+// prioritising create / update operations before deletes.
+func performTxs(queries ...deferredQs) gtserror.WithCode {
+
+ // Perform create / update
+ // operations before anything.
+ for _, q := range queries {
+ for _, create := range q.create {
+ if errWithCode := create(); errWithCode != nil {
+ return errWithCode
+ }
+ }
+ for _, update := range q.update {
+ if errWithCode := update(); errWithCode != nil {
+ return errWithCode
+ }
+ }
+ }
+
+ // Perform deletes last.
+ for _, q := range queries {
+ for _, delete := range q.delete {
+ if errWithCode := delete(); errWithCode != nil {
+ return errWithCode
+ }
+ }
+ }
+
+ return nil
}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index b8adb9bb8..22574f1d7 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -34,6 +34,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/processing/common"
"code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
"code.superseriousbusiness.org/gotosocial/internal/processing/fedi"
+ filterCommon "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
filtersv1 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v1"
filtersv2 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v2"
"code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
@@ -224,6 +225,7 @@ func NewProcessor(
processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc)
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
processor.stream = stream.New(state, oauthServer)
+ filterCommon := filterCommon.New(state)
// Instantiate the rest of the sub
// processors + pin them to this struct.
@@ -232,8 +234,8 @@ func NewProcessor(
processor.application = application.New(state, converter)
processor.conversations = conversations.New(state, converter, visFilter, muteFilter)
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
- processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
- processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
+ processor.filtersv1 = filtersv1.New(state, converter, filterCommon, &processor.stream)
+ processor.filtersv2 = filtersv2.New(state, converter, filterCommon, &processor.stream)
processor.interactionRequests = interactionrequests.New(&common, state, converter)
processor.list = list.New(state, converter)
processor.markers = markers.New(state, converter)
diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go
index 97eb813db..b4568722d 100644
--- a/internal/processing/search/util.go
+++ b/internal/processing/search/util.go
@@ -21,7 +21,6 @@ import (
"context"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -114,7 +113,7 @@ func (p *Processor) packageStatuses(
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err)
continue
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
index 6f3e7a4fd..531dff1d6 100644
--- a/internal/processing/status/context.go
+++ b/internal/processing/status/context.go
@@ -24,7 +24,6 @@ import (
"strings"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
@@ -278,7 +277,7 @@ func (p *Processor) ContextGet(
) (*apimodel.ThreadContext, gtserror.WithCode) {
// Retrieve filters as they affect
// what should be shown to requester.
- filters, err := p.state.DB.GetFiltersForAccountID(
+ filters, err := p.state.DB.GetFiltersByAccountID(
ctx, // Populate filters.
requester.ID,
)
@@ -305,7 +304,7 @@ func (p *Processor) ContextGet(
apiContext.Ancestors = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.ancestors,
- statusfilter.FilterContextThread,
+ gtsmodel.FilterContextThread,
filters,
)
@@ -313,7 +312,7 @@ func (p *Processor) ContextGet(
apiContext.Descendants = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.descendants,
- statusfilter.FilterContextThread,
+ gtsmodel.FilterContextThread,
filters,
)
diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go
index 8fc4bcfe8..74e7a4933 100644
--- a/internal/processing/stream/statusupdate_test.go
+++ b/internal/processing/stream/statusupdate_test.go
@@ -22,7 +22,7 @@ import (
"encoding/json"
"testing"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/stream"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
"github.com/stretchr/testify/suite"
@@ -39,7 +39,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
suite.NoError(errWithCode)
editedStatus := suite.testStatuses["remote_account_1_status_1"]
- apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, statusfilter.FilterContextNotifications, nil)
+ apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, gtsmodel.FilterContextNotifications, nil)
suite.NoError(err)
suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome)
diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go
index 84788a8fa..c1b44fa92 100644
--- a/internal/processing/timeline/faved.go
+++ b/internal/processing/timeline/faved.go
@@ -25,8 +25,8 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/gotosocial/internal/util"
)
@@ -56,7 +56,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth,
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go
index ba74b770c..3089f52fc 100644
--- a/internal/processing/timeline/home.go
+++ b/internal/processing/timeline/home.go
@@ -22,7 +22,6 @@ import (
"net/url"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -77,7 +76,7 @@ func (p *Processor) HomeTimelineGet(
pageQuery,
// Status filter context.
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/home_test.go b/internal/processing/timeline/home_test.go
index 184d361f0..2d0c912f8 100644
--- a/internal/processing/timeline/home_test.go
+++ b/internal/processing/timeline/home_test.go
@@ -24,7 +24,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/paging"
- "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
)
@@ -49,24 +48,20 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = >smodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = >smodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = >smodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(true),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(false),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
)
@@ -95,6 +90,11 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
// Clear the timeline to drop all cached statuses.
suite.state.Caches.Timelines.Home.Clear(requester.ID)
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
+
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
suite.FailNow(err.Error())
diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go
index c8e6bc5f1..265cd5ca2 100644
--- a/internal/processing/timeline/list.go
+++ b/internal/processing/timeline/list.go
@@ -23,7 +23,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -88,7 +87,7 @@ func (p *Processor) ListTimelineGet(
nil,
// Status filter context.
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go
index ad60fd90c..143145bb9 100644
--- a/internal/processing/timeline/notification.go
+++ b/internal/processing/timeline/notification.go
@@ -59,7 +59,7 @@ func (p *Processor) NotificationsGet(
return util.EmptyPageableResponse(), nil
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requester.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID)
if err != nil {
err = gtserror.Newf("error getting account %s filters: %w", requester.ID, err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go
index cfb58201d..d724bfaa1 100644
--- a/internal/processing/timeline/public.go
+++ b/internal/processing/timeline/public.go
@@ -21,7 +21,6 @@ import (
"context"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -79,7 +78,7 @@ func (p *Processor) publicTimelineGet(
localOnlyFalse,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
@@ -148,7 +147,7 @@ func (p *Processor) localTimelineGet(
localOnlyTrue,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go
index 5aa09f138..3320a45da 100644
--- a/internal/processing/timeline/public_test.go
+++ b/internal/processing/timeline/public_test.go
@@ -24,7 +24,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/paging"
- "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
)
@@ -110,24 +109,20 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = >smodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = >smodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = >smodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(false),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(true),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextPublic),
}
)
@@ -153,6 +148,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline")
}
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
+
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
suite.FailNow(err.Error())
diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go
index 88333d343..995f9f8cc 100644
--- a/internal/processing/timeline/tag.go
+++ b/internal/processing/timeline/tag.go
@@ -24,7 +24,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -87,7 +86,7 @@ func (p *Processor) TagTimelineGet(
nil,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go
index a86702d42..a37785879 100644
--- a/internal/processing/timeline/timeline.go
+++ b/internal/processing/timeline/timeline.go
@@ -70,7 +70,7 @@ func (p *Processor) getStatusTimeline(
page *paging.Page,
pagePath string,
pageQuery url.Values,
- filterCtx statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
loadPage func(*paging.Page) (statuses []*gtsmodel.Status, err error),
filter func(*gtsmodel.Status) (delete bool),
postFilter func(*gtsmodel.Status) (remove bool),
@@ -83,7 +83,7 @@ func (p *Processor) getStatusTimeline(
if requester != nil {
// Fetch all filters relevant for requesting account.
- filters, err = p.state.DB.GetFiltersForAccountID(ctx,
+ filters, err = p.state.DB.GetFiltersByAccountID(ctx,
requester.ID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index 3f6964259..1c30c11be 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -27,7 +27,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/messages"
@@ -213,7 +212,7 @@ func (suite *FromClientAPITestSuite) statusJSON(
ctx,
status,
requestingAccount,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil,
)
if err != nil {
diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go
index 044315349..b11fb103e 100644
--- a/internal/processing/workers/surfacenotify.go
+++ b/internal/processing/workers/surfacenotify.go
@@ -743,7 +743,7 @@ func (s *Surface) Notify(
}
}
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID)
+ filters, err := s.State.DB.GetFiltersByAccountID(ctx, targetAccount.ID)
if err != nil {
return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err)
}
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
index 7ef5fee87..7f9bcd596 100644
--- a/internal/processing/workers/surfacetimeline.go
+++ b/internal/processing/workers/surfacetimeline.go
@@ -180,7 +180,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
follow.Account,
status,
stream.TimelineHome,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
); homeTimelined {
@@ -275,7 +275,7 @@ func (s *Surface) listTimelineStatusForFollow(
follow.Account,
status,
stream.TimelineList+":"+list.ID, // key streamType to this specific list
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)
@@ -288,7 +288,7 @@ func (s *Surface) listTimelineStatusForFollow(
// getFiltersAndMutes returns an account's filters and mutes.
func (s *Surface) getFilters(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) {
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, accountID)
+ filters, err := s.State.DB.GetFiltersByAccountID(ctx, accountID)
if err != nil {
return nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err)
}
@@ -369,7 +369,7 @@ func (s *Surface) timelineStatus(
account *gtsmodel.Account,
status *gtsmodel.Status,
streamType string,
- filterCtx statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) bool {
@@ -436,7 +436,7 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers(
tagFollowerAccount,
status,
stream.TimelineHome,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)
}
@@ -731,7 +731,7 @@ func (s *Surface) timelineStreamStatusUpdate(
apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
status,
account,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 3ac7e1536..ed8f3a4cd 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -848,14 +848,14 @@ func (c *Converter) StatusToAPIStatus(
ctx context.Context,
status *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) (*apimodel.Status, error) {
return c.statusToAPIStatus(
ctx,
status,
requestingAccount,
- filterContext,
+ filterCtx,
filters,
true,
true,
@@ -870,7 +870,7 @@ func (c *Converter) statusToAPIStatus(
ctx context.Context,
status *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
placeholdAttachments bool,
addPendingNote bool,
@@ -879,7 +879,7 @@ func (c *Converter) statusToAPIStatus(
ctx,
status,
requestingAccount, // Can be nil.
- filterContext, // Can be empty.
+ filterCtx, // Can be empty.
filters,
)
if err != nil {
@@ -945,13 +945,13 @@ func (c *Converter) statusToAPIFilterResults(
ctx context.Context,
s *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) ([]apimodel.FilterResult, error) {
// If there are no filters or mutes, we're done.
// We never hide statuses authored by the requesting account,
// since not being able to see your own posts is confusing.
- if filterContext == "" || (len(filters) == 0) || s.AccountID == requestingAccount.ID {
+ if filterCtx == 0 || (len(filters) == 0) || s.AccountID == requestingAccount.ID {
return nil, nil
}
@@ -967,6 +967,7 @@ func (c *Converter) statusToAPIFilterResults(
cache := c.state.Caches.StatusesFilterableFields
fields, stored := cache.Get(statusKey)
if !stored {
+
// We don't have filterable fields
// cached, calculate + cache now.
fields = filterableFields(s)
@@ -976,7 +977,7 @@ func (c *Converter) statusToAPIFilterResults(
// Record all matching warn filters and the reasons they matched.
filterResults := make([]apimodel.FilterResult, 0, len(filters))
for _, filter := range filters {
- if !filterAppliesInContext(filter, filterContext) {
+ if !filter.Contexts.Applies(filterCtx) {
// Filter doesn't apply
// to this context.
continue
@@ -1004,7 +1005,8 @@ func (c *Converter) statusToAPIFilterResults(
}
}
- // A status has only one ID. Not clear why this is a list in the Mastodon API.
+ // A status has only one ID. Not clear
+ // why this is a list in the Mastodon API.
statusMatches := make([]string, 0, 1)
for _, filterStatus := range filter.Statuses {
if s.ID == filterStatus.StatusID {
@@ -1017,12 +1019,8 @@ func (c *Converter) statusToAPIFilterResults(
switch filter.Action {
case gtsmodel.FilterActionWarn:
// Record what matched.
- apiFilter, err := c.FilterToAPIFilterV2(ctx, filter)
- if err != nil {
- return nil, err
- }
filterResults = append(filterResults, apimodel.FilterResult{
- Filter: *apiFilter,
+ Filter: *FilterToAPIFilterV2(filter),
KeywordMatches: keywordMatches,
StatusMatches: statusMatches,
})
@@ -1037,23 +1035,6 @@ func (c *Converter) statusToAPIFilterResults(
return filterResults, nil
}
-// filterAppliesInContext returns whether a given filter applies in a given context.
-func filterAppliesInContext(filter *gtsmodel.Filter, filterContext statusfilter.FilterContext) bool {
- switch filterContext {
- case statusfilter.FilterContextHome:
- return util.PtrOrValue(filter.ContextHome, false)
- case statusfilter.FilterContextNotifications:
- return util.PtrOrValue(filter.ContextNotifications, false)
- case statusfilter.FilterContextPublic:
- return util.PtrOrValue(filter.ContextPublic, false)
- case statusfilter.FilterContextThread:
- return util.PtrOrValue(filter.ContextThread, false)
- case statusfilter.FilterContextAccount:
- return util.PtrOrValue(filter.ContextAccount, false)
- }
- return false
-}
-
// StatusToWebStatus converts a gts model status into an
// api representation suitable for serving into a web template.
//
@@ -1063,9 +1044,9 @@ func (c *Converter) StatusToWebStatus(
s *gtsmodel.Status,
) (*apimodel.WebStatus, error) {
apiStatus, err := c.statusToFrontend(ctx, s,
- nil, // No authed requester.
- statusfilter.FilterContextNone, // No filters.
- nil, // No filters.
+ nil, // No authed requester.
+ gtsmodel.FilterContextNone, // No filters.
+ nil, // No filters.
)
if err != nil {
return nil, err
@@ -1234,7 +1215,7 @@ func (c *Converter) statusToFrontend(
ctx context.Context,
status *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) (
*apimodel.Status,
@@ -1243,7 +1224,7 @@ func (c *Converter) statusToFrontend(
apiStatus, err := c.baseStatusToFrontend(ctx,
status,
requestingAccount,
- filterContext,
+ filterCtx,
filters,
)
if err != nil {
@@ -1254,7 +1235,7 @@ func (c *Converter) statusToFrontend(
reblog, err := c.baseStatusToFrontend(ctx,
status.BoostOf,
requestingAccount,
- filterContext,
+ filterCtx,
filters,
)
if errors.Is(err, statusfilter.ErrHideStatus) {
@@ -1287,7 +1268,7 @@ func (c *Converter) baseStatusToFrontend(
ctx context.Context,
s *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) (
*apimodel.Status,
@@ -1466,7 +1447,7 @@ func (c *Converter) baseStatusToFrontend(
}
// Apply filters.
- filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters)
+ filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterCtx, filters)
if err != nil {
if errors.Is(err, statusfilter.ErrHideStatus) {
return nil, err
@@ -2007,7 +1988,7 @@ func (c *Converter) NotificationToAPINotification(
apiStatus, err = c.StatusToAPIStatus(
ctx, n.Status,
n.TargetAccount,
- statusfilter.FilterContextNotifications,
+ gtsmodel.FilterContextNotifications,
filters,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
@@ -2058,7 +2039,7 @@ func (c *Converter) ConversationToAPIConversation(
ctx,
conversation.LastStatus,
requester,
- statusfilter.FilterContextNotifications,
+ gtsmodel.FilterContextNotifications,
filters,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
@@ -2327,7 +2308,7 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
ctx,
s,
requestingAccount,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil, // No filters.
true, // Placehold unknown attachments.
@@ -2558,48 +2539,53 @@ func (c *Converter) convertAttachmentsToAPIAttachments(ctx context.Context, atta
}
// FilterToAPIFiltersV1 converts one GTS model filter into an API v1 filter list
-func (c *Converter) FilterToAPIFiltersV1(ctx context.Context, filter *gtsmodel.Filter) ([]*apimodel.FilterV1, error) {
+func FilterToAPIFiltersV1(filter *gtsmodel.Filter) []*apimodel.FilterV1 {
apiFilters := make([]*apimodel.FilterV1, 0, len(filter.Keywords))
for _, filterKeyword := range filter.Keywords {
- apiFilter, err := c.FilterKeywordToAPIFilterV1(ctx, filterKeyword)
- if err != nil {
- return nil, err
- }
+ apiFilter := FilterKeywordToAPIFilterV1(filter, filterKeyword)
apiFilters = append(apiFilters, apiFilter)
}
- return apiFilters, nil
+ return apiFilters
}
// FilterKeywordToAPIFilterV1 converts one GTS model filter and filter keyword into an API v1 filter
-func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (*apimodel.FilterV1, error) {
- if filterKeyword.Filter == nil {
- return nil, gtserror.New("FilterKeyword model's Filter field isn't populated, but needs to be")
- }
- filter := filterKeyword.Filter
-
+func FilterKeywordToAPIFilterV1(filter *gtsmodel.Filter, keyword *gtsmodel.FilterKeyword) *apimodel.FilterV1 {
return &apimodel.FilterV1{
// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID.
- ID: filterKeyword.ID,
- Phrase: filterKeyword.Keyword,
+ ID: keyword.ID,
+ Phrase: keyword.Keyword,
Context: filterToAPIFilterContexts(filter),
- WholeWord: util.PtrOrValue(filterKeyword.WholeWord, false),
+ WholeWord: util.PtrOrValue(keyword.WholeWord, false),
ExpiresAt: filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt),
Irreversible: filter.Action == gtsmodel.FilterActionHide,
- }, nil
+ }
}
// FilterToAPIFilterV2 converts one GTS model filter into an API v2 filter.
-func (c *Converter) FilterToAPIFilterV2(ctx context.Context, filter *gtsmodel.Filter) (*apimodel.FilterV2, error) {
- apiFilterKeywords := make([]apimodel.FilterKeyword, 0, len(filter.Keywords))
- for _, filterKeyword := range filter.Keywords {
- apiFilterKeywords = append(apiFilterKeywords, *c.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
+func FilterToAPIFilterV2(filter *gtsmodel.Filter) *apimodel.FilterV2 {
+ apiFilterKeywords := make([]apimodel.FilterKeyword, len(filter.Keywords))
+ if len(apiFilterKeywords) != len(filter.Keywords) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
}
-
- apiFilterStatuses := make([]apimodel.FilterStatus, 0, len(filter.Keywords))
- for _, filterStatus := range filter.Statuses {
- apiFilterStatuses = append(apiFilterStatuses, *c.FilterStatusToAPIFilterStatus(ctx, filterStatus))
+ for i, filterKeyword := range filter.Keywords {
+ apiFilterKeywords[i] = apimodel.FilterKeyword{
+ ID: filterKeyword.ID,
+ Keyword: filterKeyword.Keyword,
+ WholeWord: util.PtrOrValue(filterKeyword.WholeWord, false),
+ }
+ }
+ apiFilterStatuses := make([]apimodel.FilterStatus, len(filter.Statuses))
+ if len(apiFilterStatuses) != len(filter.Statuses) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filterStatus := range filter.Statuses {
+ apiFilterStatuses[i] = apimodel.FilterStatus{
+ ID: filterStatus.ID,
+ StatusID: filterStatus.StatusID,
+ }
}
-
return &apimodel.FilterV2{
ID: filter.ID,
Title: filter.Title,
@@ -2608,7 +2594,7 @@ func (c *Converter) FilterToAPIFilterV2(ctx context.Context, filter *gtsmodel.Fi
FilterAction: filterActionToAPIFilterAction(filter.Action),
Keywords: apiFilterKeywords,
Statuses: apiFilterStatuses,
- }, nil
+ }
}
func filterExpiresAtToAPIFilterExpiresAt(expiresAt time.Time) *string {
@@ -2620,19 +2606,19 @@ func filterExpiresAtToAPIFilterExpiresAt(expiresAt time.Time) *string {
func filterToAPIFilterContexts(filter *gtsmodel.Filter) []apimodel.FilterContext {
apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues)
- if util.PtrOrValue(filter.ContextHome, false) {
+ if filter.Contexts.Home() {
apiContexts = append(apiContexts, apimodel.FilterContextHome)
}
- if util.PtrOrValue(filter.ContextNotifications, false) {
+ if filter.Contexts.Notifications() {
apiContexts = append(apiContexts, apimodel.FilterContextNotifications)
}
- if util.PtrOrValue(filter.ContextPublic, false) {
+ if filter.Contexts.Public() {
apiContexts = append(apiContexts, apimodel.FilterContextPublic)
}
- if util.PtrOrValue(filter.ContextThread, false) {
+ if filter.Contexts.Thread() {
apiContexts = append(apiContexts, apimodel.FilterContextThread)
}
- if util.PtrOrValue(filter.ContextAccount, false) {
+ if filter.Contexts.Account() {
apiContexts = append(apiContexts, apimodel.FilterContextAccount)
}
return apiContexts
@@ -2649,7 +2635,7 @@ func filterActionToAPIFilterAction(m gtsmodel.FilterAction) apimodel.FilterActio
}
// FilterKeywordToAPIFilterKeyword converts a GTS model filter status into an API filter status.
-func (c *Converter) FilterKeywordToAPIFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) *apimodel.FilterKeyword {
+func FilterKeywordToAPIFilterKeyword(filterKeyword *gtsmodel.FilterKeyword) *apimodel.FilterKeyword {
return &apimodel.FilterKeyword{
ID: filterKeyword.ID,
Keyword: filterKeyword.Keyword,
@@ -2658,7 +2644,7 @@ func (c *Converter) FilterKeywordToAPIFilterKeyword(ctx context.Context, filterK
}
// FilterStatusToAPIFilterStatus converts a GTS model filter status into an API filter status.
-func (c *Converter) FilterStatusToAPIFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus) *apimodel.FilterStatus {
+func FilterStatusToAPIFilterStatus(filterStatus *gtsmodel.FilterStatus) *apimodel.FilterStatus {
return &apimodel.FilterStatus{
ID: filterStatus.ID,
StatusID: filterStatus.StatusID,
@@ -3027,7 +3013,7 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
ctx,
req.Status,
requestingAcct,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil, // No filters.
)
if err != nil {
@@ -3041,7 +3027,7 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
ctx,
req.Reply,
requestingAcct,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil, // No filters.
true, // Placehold unknown attachments.
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 03d812556..1795180e9 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -465,7 +465,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
testStatus := suite.testStatuses["admin_account_status_1"]
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -628,7 +628,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendHTMLContentWarning
testStatus.ContentWarning = `
First paragraph of content warning
Here's the title!
Big boobs
Tee hee!
Some more text
And a bunch more
Hasta la victoria siempre!
`
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -794,7 +794,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendApplicationDeleted
}
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -971,7 +971,6 @@ func (suite *InternalToFrontendTestSuite) filteredStatusToFrontend(action gtsmod
expectedMatchingFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
suite.NoError(expectedMatchingFilterKeyword.Compile())
- expectedMatchingFilterKeyword.Filter = expectedMatchingFilter
expectedMatchingFilter.Keywords = []*gtsmodel.FilterKeyword{expectedMatchingFilterKeyword}
@@ -981,7 +980,7 @@ func (suite *InternalToFrontendTestSuite) filteredStatusToFrontend(action gtsmod
suite.T().Context(),
testStatus,
requestingAccount,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
requestingAccountFilters,
)
}
@@ -1521,20 +1520,16 @@ func (suite *InternalToFrontendTestSuite) testHashtagFilteredStatusToFrontend(wh
}
filter := >smodel.Filter{
- Action: gtsmodel.FilterActionWarn,
- Keywords: []*gtsmodel.FilterKeyword{filterKeyword},
- ContextHome: util.Ptr(true),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(false),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Action: gtsmodel.FilterActionWarn,
+ Keywords: []*gtsmodel.FilterKeyword{filterKeyword},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
suite.T().Context(),
testStatus,
requestingAccount,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
[]*gtsmodel.Filter{filter},
)
if err != nil {
@@ -1564,7 +1559,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
testStatus := suite.testStatuses["remote_account_2_status_1"]
requestingAccount := suite.testAccounts["admin_account"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -1891,7 +1886,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
*testStatus = *suite.testStatuses["admin_account_status_1"]
testStatus.Language = ""
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -2052,7 +2047,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
*testStatus = *suite.testStatuses["local_account_1_status_3"]
testStatus.Language = ""
requestingAccount := suite.testAccounts["admin_account"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -2165,7 +2160,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval()
suite.T().Context(),
testStatus,
requestingAccount,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil,
)
if err != nil {
diff --git a/test/envparsing.sh b/test/envparsing.sh
index b6d75cf07..0c737c1d9 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -42,6 +42,7 @@ EXPECT=$(cat << "EOF"
"cache-domain-permission-subscription-mem-ratio": 0.5,
"cache-emoji-category-mem-ratio": 0.1,
"cache-emoji-mem-ratio": 3,
+ "cache-filter-ids-mem-ratio": 2,
"cache-filter-keyword-mem-ratio": 0.5,
"cache-filter-mem-ratio": 0.5,
"cache-filter-status-mem-ratio": 0.5,
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 42caf59bd..d8cfb9b82 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -4091,54 +4091,45 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
func NewTestFilters() map[string]*gtsmodel.Filter {
return map[string]*gtsmodel.Filter{
"local_account_1_filter_1": {
- ID: "01HN26VM6KZTW1ANNRVSBMA461",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
- Title: "fnord",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HN26VM6KZTW1ANNRVSBMA461",
+ AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
+ Title: "fnord",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
+ KeywordIDs: []string{"01HN272TAVWAXX72ZX4M8JZ0PS"},
},
"local_account_1_filter_2": {
- ID: "01HN277FSPQAWXZXK92QPPYF79",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
- Title: "metasyntactic variables",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HN277FSPQAWXZXK92QPPYF79",
+ AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
+ Title: "metasyntactic variables",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
+ KeywordIDs: []string{"01HN277Y11ENG4EC1ERMAC9FH4", "01HN278494N88BA2FY4DZ5JTNS", "01HXATJTGYT4BTG2YASE5M7GSD"},
},
"local_account_1_filter_3": {
- ID: "01HWXQDXE4QX4R9EGMG729Y76C",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
- Title: "puppies",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HWXQDXE4QX4R9EGMG729Y76C",
+ AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
+ Title: "puppies",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
+ StatusIDs: []string{"01HWXQDY8EE182AWQKS45JV50W"},
},
"local_account_1_filter_4": {
- ID: "01HZ55WWWP82WYP2A1BKWK8Y9Q",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
- Title: "empty filter with no keywords or statuses",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HZ55WWWP82WYP2A1BKWK8Y9Q",
+ AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
+ Title: "empty filter with no keywords or statuses",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
+ KeywordIDs: []string{},
},
"local_account_2_filter_1": {
- ID: "01HNGFYJBED9FS0VWRVMY4TKXH",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1VYJAE00TVVGMM5JNJ8X",
- Title: "gamer words",
- Action: gtsmodel.FilterActionWarn,
- ContextHome: util.Ptr(true),
- ContextPublic: util.Ptr(true),
+ ID: "01HNGFYJBED9FS0VWRVMY4TKXH",
+ AccountID: "01F8MH1VYJAE00TVVGMM5JNJ8X",
+ Title: "gamer words",
+ Action: gtsmodel.FilterActionWarn,
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome | gtsmodel.FilterContextPublic),
+ KeywordIDs: []string{"01HNGG51HV2JT67XQ5MQ7RA1WE"},
+ StatusIDs: []string{"01HX9WXVEH05E78ABR81FZFFFY"},
},
}
}
@@ -4147,45 +4138,30 @@ func NewTestFilterKeywords() map[string]*gtsmodel.FilterKeyword {
return map[string]*gtsmodel.FilterKeyword{
"local_account_1_filter_1_keyword_1": {
ID: "01HN272TAVWAXX72ZX4M8JZ0PS",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
FilterID: "01HN26VM6KZTW1ANNRVSBMA461",
Keyword: "fnord",
WholeWord: util.Ptr(true),
},
"local_account_1_filter_2_keyword_1": {
ID: "01HN277Y11ENG4EC1ERMAC9FH4",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
FilterID: "01HN277FSPQAWXZXK92QPPYF79",
Keyword: "foo",
WholeWord: util.Ptr(true),
},
"local_account_1_filter_2_keyword_2": {
ID: "01HN278494N88BA2FY4DZ5JTNS",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
FilterID: "01HN277FSPQAWXZXK92QPPYF79",
Keyword: "bar",
WholeWord: util.Ptr(true),
},
"local_account_1_filter_2_keyword_3": {
ID: "01HXATJTGYT4BTG2YASE5M7GSD",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
FilterID: "01HN277FSPQAWXZXK92QPPYF79",
Keyword: "quux",
WholeWord: util.Ptr(true),
},
"local_account_2_filter_1_keyword_1": {
ID: "01HNGG51HV2JT67XQ5MQ7RA1WE",
- CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
- AccountID: "01F8MH1VYJAE00TVVGMM5JNJ8X",
FilterID: "01HNGFYJBED9FS0VWRVMY4TKXH",
Keyword: "Virtual Boy",
WholeWord: util.Ptr(true),
@@ -4196,20 +4172,14 @@ func NewTestFilterKeywords() map[string]*gtsmodel.FilterKeyword {
func NewTestFilterStatuses() map[string]*gtsmodel.FilterStatus {
return map[string]*gtsmodel.FilterStatus{
"local_account_1_filter_3_status_1": {
- ID: "01HWXQDY8EE182AWQKS45JV50W",
- CreatedAt: time.Time{},
- UpdatedAt: time.Time{},
- AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
- FilterID: "01HWXQDXE4QX4R9EGMG729Y76C",
- StatusID: "01F8MHAAY43M6RJ473VQFCVH37",
+ ID: "01HWXQDY8EE182AWQKS45JV50W",
+ FilterID: "01HWXQDXE4QX4R9EGMG729Y76C",
+ StatusID: "01F8MHAAY43M6RJ473VQFCVH37",
},
"local_account_2_filter_1_status_1": {
- ID: "01HX9WXVEH05E78ABR81FZFFFY",
- CreatedAt: time.Time{},
- UpdatedAt: time.Time{},
- AccountID: "01F8MH1VYJAE00TVVGMM5JNJ8X",
- FilterID: "01HNGFYJBED9FS0VWRVMY4TKXH",
- StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
+ ID: "01HX9WXVEH05E78ABR81FZFFFY",
+ FilterID: "01HNGFYJBED9FS0VWRVMY4TKXH",
+ StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
},
}
}