mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
38
internal/processing/filters/v2/convert.go
Normal file
38
internal/processing/filters/v2/convert.go
Normal file
@@ -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
|
||||
}
|
75
internal/processing/filters/v2/create.go
Normal file
75
internal/processing/filters/v2/create.go
Normal file
@@ -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)
|
||||
}
|
53
internal/processing/filters/v2/delete.go
Normal file
53
internal/processing/filters/v2/delete.go
Normal file
@@ -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
|
||||
}
|
35
internal/processing/filters/v2/filters.go
Normal file
35
internal/processing/filters/v2/filters.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
81
internal/processing/filters/v2/get.go
Normal file
81
internal/processing/filters/v2/get.go
Normal file
@@ -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
|
||||
}
|
67
internal/processing/filters/v2/keywordcreate.go
Normal file
67
internal/processing/filters/v2/keywordcreate.go
Normal file
@@ -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
|
||||
}
|
53
internal/processing/filters/v2/keyworddelete.go
Normal file
53
internal/processing/filters/v2/keyworddelete.go
Normal file
@@ -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
|
||||
}
|
89
internal/processing/filters/v2/keywordget.go
Normal file
89
internal/processing/filters/v2/keywordget.go
Normal file
@@ -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
|
||||
}
|
66
internal/processing/filters/v2/keywordupdate.go
Normal file
66
internal/processing/filters/v2/keywordupdate.go
Normal file
@@ -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
|
||||
}
|
66
internal/processing/filters/v2/statuscreate.go
Normal file
66
internal/processing/filters/v2/statuscreate.go
Normal file
@@ -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
|
||||
}
|
53
internal/processing/filters/v2/statusdelete.go
Normal file
53
internal/processing/filters/v2/statusdelete.go
Normal file
@@ -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
|
||||
}
|
89
internal/processing/filters/v2/statusget.go
Normal file
89
internal/processing/filters/v2/statusget.go
Normal file
@@ -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
|
||||
}
|
125
internal/processing/filters/v2/update.go
Normal file
125
internal/processing/filters/v2/update.go
Normal file
@@ -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)
|
||||
}
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user