summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2024-05-31 03:55:56 -0700
committerLibravatar GitHub <noreply@github.com>2024-05-31 12:55:56 +0200
commit61a8d362557c1787d534024ed2f14e999b785cc3 (patch)
treef0ace4432170c0c88afa3233c2c327c808b7b92d /internal/processing
parent[chore] little startup tweaks (#2941) (diff)
downloadgotosocial-61a8d362557c1787d534024ed2f14e999b785cc3.tar.xz
[feature] Implement Filter API v2 (#2936)
* Use correct entity name * We support server-side filters now * Document filter v1 methods that can throw a 409 * Validate v1 filter phrase as filter title * Always check v1 filter API status codes in tests * Document keyword minimum requirement on filter API v1 * Make it possible to specify filter keyword update columns per filter keyword * Implement v2 filter API * Fix lint and tests * Update Swagger spec * Fix filter update test * Update Swagger spec *correctly* * Update actual files Swagger spec was generated from * Remove keywords_attributes and statuses_attributes * Add test for serialization of empty filter * More helpful messages when object is owned by wrong account
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/filters/v1/get.go4
-rw-r--r--internal/processing/filters/v1/update.go8
-rw-r--r--internal/processing/filters/v2/convert.go38
-rw-r--r--internal/processing/filters/v2/create.go75
-rw-r--r--internal/processing/filters/v2/delete.go53
-rw-r--r--internal/processing/filters/v2/filters.go35
-rw-r--r--internal/processing/filters/v2/get.go81
-rw-r--r--internal/processing/filters/v2/keywordcreate.go67
-rw-r--r--internal/processing/filters/v2/keyworddelete.go53
-rw-r--r--internal/processing/filters/v2/keywordget.go89
-rw-r--r--internal/processing/filters/v2/keywordupdate.go66
-rw-r--r--internal/processing/filters/v2/statuscreate.go66
-rw-r--r--internal/processing/filters/v2/statusdelete.go53
-rw-r--r--internal/processing/filters/v2/statusget.go89
-rw-r--r--internal/processing/filters/v2/update.go125
-rw-r--r--internal/processing/processor.go7
16 files changed, 904 insertions, 5 deletions
diff --git a/internal/processing/filters/v1/get.go b/internal/processing/filters/v1/get.go
index 39575dd94..3ead09b20 100644
--- a/internal/processing/filters/v1/get.go
+++ b/internal/processing/filters/v1/get.go
@@ -59,8 +59,8 @@ func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*a
}
apiFilters := make([]*apimodel.FilterV1, 0, len(filters))
- for _, list := range filters {
- apiFilter, errWithCode := p.apiFilter(ctx, list)
+ for _, filter := range filters {
+ apiFilter, errWithCode := p.apiFilter(ctx, filter)
if errWithCode != nil {
return nil, errWithCode
}
diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go
index 1fe49721b..0421dc786 100644
--- a/internal/processing/filters/v1/update.go
+++ b/internal/processing/filters/v1/update.go
@@ -149,9 +149,11 @@ func (p *Processor) Update(
"context_thread",
"context_account",
}
- filterKeywordColumns := []string{
- "keyword",
- "whole_word",
+ filterKeywordColumns := [][]string{
+ {
+ "keyword",
+ "whole_word",
+ },
}
if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, nil, nil); err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
diff --git a/internal/processing/filters/v2/convert.go b/internal/processing/filters/v2/convert.go
new file mode 100644
index 000000000..1e544e6e4
--- /dev/null
+++ b/internal/processing/filters/v2/convert.go
@@ -0,0 +1,38 @@
+// 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 "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// apiFilter is a shortcut to return the API v2 filter version of the given
+// filter, or return an appropriate error if conversion fails.
+func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) {
+ apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err))
+ }
+
+ return apiFilter, nil
+}
diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go
new file mode 100644
index 000000000..c7b500e9e
--- /dev/null
+++ b/internal/processing/filters/v2/create.go
@@ -0,0 +1,75 @@
+// 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"
+ "errors"
+ "fmt"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/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) {
+ filter := &gtsmodel.Filter{
+ ID: id.NewULID(),
+ AccountID: account.ID,
+ Title: form.Title,
+ Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction),
+ }
+ if form.ExpiresIn != nil {
+ 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),
+ )
+ }
+ }
+
+ 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)
+ }
+
+ return p.apiFilter(ctx, filter)
+}
diff --git a/internal/processing/filters/v2/delete.go b/internal/processing/filters/v2/delete.go
new file mode 100644
index 000000000..b1bebdcb6
--- /dev/null
+++ b/internal/processing/filters/v2/delete.go
@@ -0,0 +1,53 @@
+// 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"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// 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,
+ 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)
+ }
+
+ // 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 {
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/filters/v2/filters.go b/internal/processing/filters/v2/filters.go
new file mode 100644
index 000000000..dfb6a8992
--- /dev/null
+++ b/internal/processing/filters/v2/filters.go
@@ -0,0 +1,35 @@
+// 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 (
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+type Processor struct {
+ state *state.State
+ converter *typeutils.Converter
+}
+
+func New(state *state.State, converter *typeutils.Converter) Processor {
+ return Processor{
+ state: state,
+ converter: converter,
+ }
+}
diff --git a/internal/processing/filters/v2/get.go b/internal/processing/filters/v2/get.go
new file mode 100644
index 000000000..39b937eb2
--- /dev/null
+++ b/internal/processing/filters/v2/get.go
@@ -0,0 +1,81 @@
+// 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"
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// 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)
+ }
+ 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)
+}
+
+// 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,
+ )
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ return nil, nil
+ }
+ 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)
+ }
+
+ // Sort them by ID so that they're in a stable order.
+ // Clients may opt to sort them lexically in a locale-aware manner.
+ slices.SortFunc(apiFilters, func(lhs *apimodel.FilterV2, rhs *apimodel.FilterV2) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ return apiFilters, nil
+}
diff --git a/internal/processing/filters/v2/keywordcreate.go b/internal/processing/filters/v2/keywordcreate.go
new file mode 100644
index 000000000..711b855fa
--- /dev/null
+++ b/internal/processing/filters/v2/keywordcreate.go
@@ -0,0 +1,67 @@
+// 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"
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+)
+
+// 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),
+ )
+ }
+
+ filterKeyword := &gtsmodel.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())
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+}
diff --git a/internal/processing/filters/v2/keyworddelete.go b/internal/processing/filters/v2/keyworddelete.go
new file mode 100644
index 000000000..edf57167d
--- /dev/null
+++ b/internal/processing/filters/v2/keyworddelete.go
@@ -0,0 +1,53 @@
+// 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"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// KeywordDelete deletes an existing filter keyword from a filter.
+func (p *Processor) KeywordDelete(
+ ctx context.Context,
+ account *gtsmodel.Account,
+ filterID string,
+) gtserror.WithCode {
+ // Get the filter keyword.
+ filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterID)
+ if err != nil {
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ // 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 {
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/filters/v2/keywordget.go b/internal/processing/filters/v2/keywordget.go
new file mode 100644
index 000000000..5f5a63b26
--- /dev/null
+++ b/internal/processing/filters/v2/keywordget.go
@@ -0,0 +1,89 @@
+// 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"
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// 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)
+ }
+ 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
+}
+
+// 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)
+ }
+
+ filterKeywords, err := p.state.DB.GetFilterKeywordsForFilterID(
+ ctx,
+ filter.ID,
+ )
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ return nil, nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiFilterKeywords := make([]*apimodel.FilterKeyword, 0, len(filterKeywords))
+ for _, filterKeyword := range filterKeywords {
+ apiFilterKeywords = append(apiFilterKeywords, p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
+ }
+
+ // Sort them by ID so that they're in a stable order.
+ // Clients may opt to sort them lexically in a locale-aware manner.
+ slices.SortFunc(apiFilterKeywords, func(lhs *apimodel.FilterKeyword, rhs *apimodel.FilterKeyword) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ return apiFilterKeywords, nil
+}
diff --git a/internal/processing/filters/v2/keywordupdate.go b/internal/processing/filters/v2/keywordupdate.go
new file mode 100644
index 000000000..9a4058c23
--- /dev/null
+++ b/internal/processing/filters/v2/keywordupdate.go
@@ -0,0 +1,66 @@
+// 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"
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// 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,
+ 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),
+ )
+ }
+
+ 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())
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+}
diff --git a/internal/processing/filters/v2/statuscreate.go b/internal/processing/filters/v2/statuscreate.go
new file mode 100644
index 000000000..a211dec2e
--- /dev/null
+++ b/internal/processing/filters/v2/statuscreate.go
@@ -0,0 +1,66 @@
+// 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"
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+)
+
+// 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),
+ )
+ }
+
+ filterStatus := &gtsmodel.FilterStatus{
+ ID: id.NewULID(),
+ AccountID: account.ID,
+ 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())
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
+}
diff --git a/internal/processing/filters/v2/statusdelete.go b/internal/processing/filters/v2/statusdelete.go
new file mode 100644
index 000000000..a428e7409
--- /dev/null
+++ b/internal/processing/filters/v2/statusdelete.go
@@ -0,0 +1,53 @@
+// 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"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// StatusDelete deletes an existing filter status from a filter.
+func (p *Processor) StatusDelete(
+ ctx context.Context,
+ account *gtsmodel.Account,
+ filterID string,
+) gtserror.WithCode {
+ // Get the filter status.
+ filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterID)
+ if err != nil {
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ // 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 {
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/filters/v2/statusget.go b/internal/processing/filters/v2/statusget.go
new file mode 100644
index 000000000..197a3872e
--- /dev/null
+++ b/internal/processing/filters/v2/statusget.go
@@ -0,0 +1,89 @@
+// 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"
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// 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)
+ }
+ 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
+}
+
+// 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)
+ }
+
+ filterStatuses, err := p.state.DB.GetFilterStatusesForFilterID(
+ ctx,
+ filter.ID,
+ )
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ return nil, nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiFilterStatuses := make([]*apimodel.FilterStatus, 0, len(filterStatuses))
+ for _, filterStatus := range filterStatuses {
+ apiFilterStatuses = append(apiFilterStatuses, p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus))
+ }
+
+ // Sort them by ID so that they're in a stable order.
+ // Clients may opt to sort them by status ID instead.
+ slices.SortFunc(apiFilterStatuses, func(lhs *apimodel.FilterStatus, rhs *apimodel.FilterStatus) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ return apiFilterStatuses, nil
+}
diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go
new file mode 100644
index 000000000..aecb53337
--- /dev/null
+++ b/internal/processing/filters/v2/update.go
@@ -0,0 +1,125 @@
+// 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"
+ "errors"
+ "fmt"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/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,
+ filterID string,
+ form *apimodel.FilterUpdateRequestV2,
+) (*apimodel.FilterV2, 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)
+ }
+ // TODO: (Vyr) is it possible to unset a filter expiration with this API?
+ if form.ExpiresIn != nil {
+ filterColumns = append(filterColumns, "expires_at")
+ filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.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),
+ )
+ }
+ }
+ }
+
+ // Temporarily detach keywords and statuses from filter, since we're not updating them below.
+ filterKeywords := filter.Keywords
+ filterStatuses := filter.Statuses
+ filter.Keywords = nil
+ filter.Statuses = nil
+
+ if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, nil, 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())
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Re-attach keywords and statuses before returning.
+ filter.Keywords = filterKeywords
+ filter.Statuses = filterStatuses
+
+ return p.apiFilter(ctx, filter)
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 4aaa94fb7..8a18bc45e 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -30,6 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
+ filtersv2 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v2"
"github.com/superseriousbusiness/gotosocial/internal/processing/list"
"github.com/superseriousbusiness/gotosocial/internal/processing/markers"
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
@@ -73,6 +74,7 @@ type Processor struct {
admin admin.Processor
fedi fedi.Processor
filtersv1 filtersv1.Processor
+ filtersv2 filtersv2.Processor
list list.Processor
markers markers.Processor
media media.Processor
@@ -102,6 +104,10 @@ func (p *Processor) FiltersV1() *filtersv1.Processor {
return &p.filtersv1
}
+func (p *Processor) FiltersV2() *filtersv2.Processor {
+ return &p.filtersv2
+}
+
func (p *Processor) List() *list.Processor {
return &p.list
}
@@ -184,6 +190,7 @@ func NewProcessor(
processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
processor.fedi = fedi.New(state, &common, converter, federator, filter)
processor.filtersv1 = filtersv1.New(state, converter)
+ processor.filtersv2 = filtersv2.New(state, converter)
processor.list = list.New(state, converter)
processor.markers = markers.New(state, converter)
processor.polls = polls.New(&common, state, converter)