summaryrefslogtreecommitdiff
path: root/internal/processing/filters/v1/update.go
blob: 15c5de3650a59c4c96326a022c36d2bd308acf63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package v1

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	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/util"
)

// Update an existing filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Update(
	ctx context.Context,
	account *gtsmodel.Account,
	filterKeywordID string,
	form *apimodel.FilterCreateUpdateRequestV1,
) (*apimodel.FilterV1, gtserror.WithCode) {
	// Get enough of the filter keyword that we can look up its filter ID.
	filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
	if err != nil {
		if errors.Is(err, db.ErrNoEntries) {
			return nil, gtserror.NewErrorNotFound(err)
		}
		return nil, gtserror.NewErrorInternalError(err)
	}
	if filterKeyword.AccountID != account.ID {
		return nil, gtserror.NewErrorNotFound(nil)
	}

	// Get the filter for this keyword.
	filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID)
	if err != nil {
		if errors.Is(err, db.ErrNoEntries) {
			return nil, gtserror.NewErrorNotFound(err)
		}
		return nil, gtserror.NewErrorInternalError(err)
	}

	title := form.Phrase
	action := gtsmodel.FilterActionWarn
	if *form.Irreversible {
		action = gtsmodel.FilterActionHide
	}
	expiresAt := time.Time{}
	if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
		expiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
	}
	contextHome := false
	contextNotifications := false
	contextPublic := false
	contextThread := false
	contextAccount := false
	for _, context := range form.Context {
		switch context {
		case apimodel.FilterContextHome:
			contextHome = true
		case apimodel.FilterContextNotifications:
			contextNotifications = true
		case apimodel.FilterContextPublic:
			contextPublic = true
		case apimodel.FilterContextThread:
			contextThread = true
		case apimodel.FilterContextAccount:
			contextAccount = true
		default:
			return nil, gtserror.NewErrorUnprocessableEntity(
				fmt.Errorf("unsupported filter context '%s'", context),
			)
		}
	}

	// v1 filter APIs can't change certain fields for a filter with multiple keywords or any statuses,
	// since it would be an unexpected side effect on filters that, to the v1 API, appear separate.
	// See https://docs.joinmastodon.org/methods/filters/#update-v1
	if len(filter.Keywords) > 1 || len(filter.Statuses) > 0 {
		forbiddenFields := make([]string, 0, 4)
		if title != filter.Title {
			forbiddenFields = append(forbiddenFields, "phrase")
		}
		if action != filter.Action {
			forbiddenFields = append(forbiddenFields, "irreversible")
		}
		if expiresAt != filter.ExpiresAt {
			forbiddenFields = append(forbiddenFields, "expires_in")
		}
		if contextHome != util.PtrOrValue(filter.ContextHome, false) ||
			contextNotifications != util.PtrOrValue(filter.ContextNotifications, false) ||
			contextPublic != util.PtrOrValue(filter.ContextPublic, false) ||
			contextThread != util.PtrOrValue(filter.ContextThread, false) ||
			contextAccount != util.PtrOrValue(filter.ContextAccount, false) {
			forbiddenFields = append(forbiddenFields, "context")
		}
		if len(forbiddenFields) > 0 {
			return nil, gtserror.NewErrorUnprocessableEntity(
				fmt.Errorf("v1 filter backwards compatibility: can't change these fields for a filter with multiple keywords or any statuses: %s", strings.Join(forbiddenFields, ", ")),
			)
		}
	}

	// Now that we've checked that the changes are legal, apply them to the filter and keyword.
	filter.Title = title
	filter.Action = action
	filter.ExpiresAt = expiresAt
	filter.ContextHome = &contextHome
	filter.ContextNotifications = &contextNotifications
	filter.ContextPublic = &contextPublic
	filter.ContextThread = &contextThread
	filter.ContextAccount = &contextAccount
	filterKeyword.Keyword = form.Phrase
	filterKeyword.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))

	// We only want to update the relevant filter keyword.
	filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
	filter.Statuses = nil
	filterKeyword.Filter = filter

	filterColumns := []string{
		"title",
		"action",
		"expires_at",
		"context_home",
		"context_notifications",
		"context_public",
		"context_thread",
		"context_account",
	}
	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) {
			err = errors.New("you already have a filter with this title")
			return nil, gtserror.NewErrorConflict(err, err.Error())
		}
		return nil, gtserror.NewErrorInternalError(err)
	}

	apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
	if errWithCode != nil {
		return nil, errWithCode
	}

	// Send a filters changed event.
	p.stream.FiltersChanged(ctx, account)

	return apiFilter, nil
}