diff options
author | 2024-05-31 03:55:56 -0700 | |
---|---|---|
committer | 2024-05-31 12:55:56 +0200 | |
commit | 61a8d362557c1787d534024ed2f14e999b785cc3 (patch) | |
tree | f0ace4432170c0c88afa3233c2c327c808b7b92d /internal/processing/filters | |
parent | [chore] little startup tweaks (#2941) (diff) | |
download | gotosocial-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/filters')
-rw-r--r-- | internal/processing/filters/v1/get.go | 4 | ||||
-rw-r--r-- | internal/processing/filters/v1/update.go | 8 | ||||
-rw-r--r-- | internal/processing/filters/v2/convert.go | 38 | ||||
-rw-r--r-- | internal/processing/filters/v2/create.go | 75 | ||||
-rw-r--r-- | internal/processing/filters/v2/delete.go | 53 | ||||
-rw-r--r-- | internal/processing/filters/v2/filters.go | 35 | ||||
-rw-r--r-- | internal/processing/filters/v2/get.go | 81 | ||||
-rw-r--r-- | internal/processing/filters/v2/keywordcreate.go | 67 | ||||
-rw-r--r-- | internal/processing/filters/v2/keyworddelete.go | 53 | ||||
-rw-r--r-- | internal/processing/filters/v2/keywordget.go | 89 | ||||
-rw-r--r-- | internal/processing/filters/v2/keywordupdate.go | 66 | ||||
-rw-r--r-- | internal/processing/filters/v2/statuscreate.go | 66 | ||||
-rw-r--r-- | internal/processing/filters/v2/statusdelete.go | 53 | ||||
-rw-r--r-- | internal/processing/filters/v2/statusget.go | 89 | ||||
-rw-r--r-- | internal/processing/filters/v2/update.go | 125 |
15 files changed, 897 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 := >smodel.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 := >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()) + } + 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 := >smodel.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) +} |