mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-28 15:52:26 -05:00
[performance] filter model and database table improvements (#4277)
- removes unnecessary fields / columns (created_at, updated_at)
- replaces filter.context_* columns with singular filter.contexts bit field which should save both struct memory and database space
- replaces filter.action string with integer enum type which should save both struct memory and database space
- adds links from filter to filter_* tables with Filter{}.KeywordIDs and Filter{}.StatusIDs fields (this also means we now have those ID slices cached, which reduces some lookups)
- removes account_id fields from filter_* tables, since there's a more direct connection between filter and filter_* tables, and filter.account_id already exists
- refactors a bunch of the filter processor logic to save on code repetition, factor in the above changes, fix a few bugs with missed error returns and bring it more in-line with some of our newer code
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4277
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
9d5af6c3dc
commit
996da6e029
82 changed files with 2440 additions and 1722 deletions
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
|
|
@ -86,6 +86,7 @@ func (c *Caches) Init() {
|
|||
c.initDomainPermissionExclude()
|
||||
c.initEmoji()
|
||||
c.initEmojiCategory()
|
||||
c.initFilterIDs()
|
||||
c.initFilter()
|
||||
c.initFilterKeyword()
|
||||
c.initFilterStatus()
|
||||
|
|
|
|||
48
internal/cache/db.go
vendored
48
internal/cache/db.go
vendored
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
23
internal/cache/invalidate.go
vendored
23
internal/cache/invalidate.go
vendored
|
|
@ -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)
|
||||
|
|
|
|||
20
internal/cache/size.go
vendored
20
internal/cache/size.go
vendored
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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.
|
||||
}
|
||||
|
|
@ -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 {
|
||||
//<editor-fold desc="Filter methods">
|
||||
|
||||
// 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
|
||||
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="Filter keyword methods">
|
||||
// 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
|
||||
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="Filter status methods">
|
||||
// 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
|
||||
|
||||
//</editor-fold>
|
||||
// DeleteFilterStatusesByIDs deletes filter statuses with the given ids.
|
||||
DeleteFilterStatusesByIDs(ctx context.Context, ids ...string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
184
internal/processing/filters/common/common.go
Normal file
184
internal/processing/filters/common/common.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `<p>First paragraph of content warning</p><h4>Here's the title!</h4><p></p><p>Big boobs<br>Tee hee!<br><br>Some more text<br>And a bunch more<br><br>Hasta la victoria siempre!</p>`
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue