[feature] Instance rules (#2125)

* init instance rules database model, admin api

* expose instance rules in public instance api

* public /api/v1/instance/rules route

* GET ruleById

* createRule route

* createRule auth check

* updateRule

* deleteRule

* list rules on about page

* ruleGet auth

* add about page ids for anchors

* process and store adding violated rules to reports

* admin api models for instance rules

* instance rule edit frontend

* change rule inputs to textareas

* database fixes after rebase (#2124)

* remove unused imports

* fix db migration column name

* fix tests

* fix more tests

* fix postgres error with wrongly used Ident

* add some tests, fiddle with rule model a bit, fix postgres migration

* swagger docs

---------

Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
f0x52 2023-08-19 14:33:15 +02:00 committed by GitHub
commit 92de8fb396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2189 additions and 107 deletions

View file

@ -72,6 +72,7 @@ type DBService struct {
db.Notification
db.Relationship
db.Report
db.Rule
db.Search
db.Session
db.Status
@ -216,6 +217,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
db: db,
state: state,
},
Rule: &ruleDB{
db: db,
state: state,
},
Search: &searchDB{
db: db,
state: state,

View file

@ -51,6 +51,7 @@ type BunDBStandardTestSuite struct {
testListEntries map[string]*gtsmodel.ListEntry
testAccountNotes map[string]*gtsmodel.AccountNote
testMarkers map[string]*gtsmodel.Marker
testRules map[string]*gtsmodel.Rule
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@ -72,6 +73,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testListEntries = testrig.NewTestListEntries()
suite.testAccountNotes = testrig.NewTestAccountNotes()
suite.testMarkers = testrig.NewTestMarkers()
suite.testRules = testrig.NewTestRules()
}
func (suite *BunDBStandardTestSuite) SetupTest() {

View file

@ -151,6 +151,16 @@ func (i *instanceDB) getInstance(ctx context.Context, lookup string, dbQuery fun
return nil, err
}
if instance.Domain == config.GetHost() {
// also populate Rules
rules, err := i.state.DB.GetActiveRules(ctx)
if err != nil {
log.Error(ctx, err)
} else {
instance.Rules = rules
}
}
return &instance, nil
}, keyParts...)
if err != nil {

View file

@ -0,0 +1,47 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
if _, err := tx.NewCreateTable().Model(&gtsmodel.Rule{}).IfNotExists().Exec(ctx); err != nil {
return err
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View 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 migrations
import (
"context"
"strings"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
if db.Dialect().Name() == dialect.SQLite { // sqlite does not have an array type
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? VARCHAR", bun.Ident("reports"), bun.Ident("rules"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
} else {
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? VARCHAR[]", bun.Ident("reports"), bun.Ident("rules"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
}
return nil
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -186,6 +186,19 @@ func (r *reportDB) PopulateReport(ctx context.Context, report *gtsmodel.Report)
}
}
if l := len(report.RuleIDs); l > 0 && l != len(report.Rules) {
// Report target rules not set, fetch from the database.
for _, v := range report.RuleIDs {
rule, err := r.state.DB.GetRuleByID(ctx, v)
if err != nil {
errs.Appendf("error populating report rules: %w", err)
} else {
report.Rules = append(report.Rules, rule)
}
}
}
if report.ActionTakenByAccountID != "" &&
report.ActionTakenByAccount == nil {
// Report action account is not set, fetch from the database.

149
internal/db/bundb/rule.go Normal file
View file

@ -0,0 +1,149 @@
// 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 bundb
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
type ruleDB struct {
db *DB
state *state.State
}
func (r *ruleDB) GetRuleByID(ctx context.Context, id string) (*gtsmodel.Rule, error) {
var rule gtsmodel.Rule
q := r.db.
NewSelect().
Model(&rule).
Where("? = ?", bun.Ident("rule.id"), id)
if err := q.Scan(ctx); err != nil {
return nil, err
}
return &rule, nil
}
func (r *ruleDB) GetRulesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Rule, error) {
rules := make([]*gtsmodel.Rule, 0, len(ids))
for _, id := range ids {
// Attempt to fetch status from DB.
rule, err := r.GetRuleByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting rule %q: %v", id, err)
continue
}
// Append status to return slice.
rules = append(rules, rule)
}
return rules, nil
}
func (r *ruleDB) GetActiveRules(ctx context.Context) ([]gtsmodel.Rule, error) {
rules := make([]gtsmodel.Rule, 0)
q := r.db.
NewSelect().
Model(&rules).
// Ignore deleted (ie., inactive) rules.
Where("? = ?", bun.Ident("rule.deleted"), false).
Order("rule.order ASC")
if err := q.Scan(ctx); err != nil {
return nil, err
}
return rules, nil
}
func (r *ruleDB) PutRule(ctx context.Context, rule *gtsmodel.Rule) error {
var lastRuleOrder uint
// Select highest existing rule order.
err := r.db.
NewSelect().
TableExpr("? AS ?", bun.Ident("rules"), bun.Ident("rule")).
Column("rule.order").
Order("rule.order DESC").
Limit(1).
Scan(ctx, &lastRuleOrder)
switch {
case errors.Is(err, db.ErrNoEntries):
// No rules set yet, index from 0.
rule.Order = util.Ptr(uint(0))
case err != nil:
// Real db error.
return err
default:
// No error means previous rule(s)
// existed. New rule order should
// be 1 higher than previous rule.
rule.Order = func() *uint {
o := lastRuleOrder + 1
return &o
}()
}
if _, err := r.db.
NewInsert().
Model(rule).
Exec(ctx); err != nil {
return err
}
// invalidate cached local instance response, so it gets updated with the new rules
r.state.Caches.GTS.Instance().Invalidate("Domain", config.GetHost())
return nil
}
func (r *ruleDB) UpdateRule(ctx context.Context, rule *gtsmodel.Rule) (*gtsmodel.Rule, error) {
// Update the rule's last-updated
rule.UpdatedAt = time.Now()
if _, err := r.db.
NewUpdate().
Model(rule).
WherePK().
Exec(ctx); err != nil {
return nil, err
}
// invalidate cached local instance response, so it gets updated with the new rules
r.state.Caches.GTS.Instance().Invalidate("Domain", config.GetHost())
return rule, nil
}

View file

@ -0,0 +1,122 @@
// 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 bundb_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
)
type RuleTestSuite struct {
BunDBStandardTestSuite
}
func (suite *RuleTestSuite) TestPutRuleWithExisting() {
r := &gtsmodel.Rule{
ID: id.NewULID(),
Text: "Pee pee poo poo",
}
if err := suite.state.DB.PutRule(context.Background(), r); err != nil {
suite.FailNow(err.Error())
}
suite.Equal(uint(len(suite.testRules)), *r.Order)
}
func (suite *RuleTestSuite) TestPutRuleNoExisting() {
var (
ctx = context.Background()
whereAny = []db.Where{{Key: "id", Value: "", Not: true}}
)
// Wipe all existing rules from the DB.
if err := suite.state.DB.DeleteWhere(
ctx,
whereAny,
&[]*gtsmodel.Rule{},
); err != nil {
suite.FailNow(err.Error())
}
r := &gtsmodel.Rule{
ID: id.NewULID(),
Text: "Pee pee poo poo",
}
if err := suite.state.DB.PutRule(ctx, r); err != nil {
suite.FailNow(err.Error())
}
// New rule is now only rule.
suite.EqualValues(uint(0), *r.Order)
}
func (suite *RuleTestSuite) TestGetRuleByID() {
rule, err := suite.state.DB.GetRuleByID(
context.Background(),
suite.testRules["rule1"].ID,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.NotNil(rule)
}
func (suite *RuleTestSuite) TestGetRulesByID() {
ruleIDs := make([]string, 0, len(suite.testRules))
for _, rule := range suite.testRules {
ruleIDs = append(ruleIDs, rule.ID)
}
rules, err := suite.state.DB.GetRulesByIDs(
context.Background(),
ruleIDs,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(rules, len(suite.testRules))
}
func (suite *RuleTestSuite) TestGetActiveRules() {
var activeRules int
for _, rule := range suite.testRules {
if !*rule.Deleted {
activeRules++
}
}
rules, err := suite.state.DB.GetActiveRules(context.Background())
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(rules, activeRules)
}
func TestRuleTestSuite(t *testing.T) {
suite.Run(t, new(RuleTestSuite))
}

View file

@ -38,6 +38,7 @@ type DB interface {
Notification
Relationship
Report
Rule
Search
Session
Status

42
internal/db/rule.go Normal file
View file

@ -0,0 +1,42 @@
// 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 db
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// Rule handles getting/creation/deletion/updating of instance rules.
type Rule interface {
// GetRuleByID gets one rule by its db id.
GetRuleByID(ctx context.Context, id string) (*gtsmodel.Rule, error)
// GetRulesByIDs gets multiple rules by their db idd.
GetRulesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Rule, error)
// GetRules gets all active (not deleted) rules.
GetActiveRules(ctx context.Context) ([]gtsmodel.Rule, error)
// PutRule puts the given rule in the database.
PutRule(ctx context.Context, rule *gtsmodel.Rule) error
// UpdateRule updates one rule by its db id.
UpdateRule(ctx context.Context, rule *gtsmodel.Rule) (*gtsmodel.Rule, error)
}