Compare commits

..

6 commits

Author SHA1 Message Date
tobi d33c738fef pee pe poo po 2024-11-21 18:20:19 +01:00
tobi 301543616b
[feature] Add domain permission drafts and excludes (#3547)
* [feature] Add domain permission drafts and excludes

* fix typescript complaining

* lint

* make filenames more consistent

* test own domain excluded
2024-11-21 13:09:58 +00:00
tobi c2029df9bc
[feature] Allow emoji shortcode to be 1-character length (#3556)
* [feature] Allow emoji shortcode to be 1-character length

* testerino fixeroni

* spaghet
2024-11-21 12:13:55 +01:00
dependabot[bot] daf55ba6a5
[chore] Bump cross-spawn from 7.0.3 to 7.0.6 in /web/source (#3552)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 11:06:41 +01:00
Jannis 9ace025da1
[bugfix] post counters should not include direct messages (#3554)
* [bugfix] post counters should not include direct messages #3504

The fix is relativly simple, it just adds a line to the relevant
function which excludes all private posts.

* Formating fix

* mb
2024-11-21 11:06:06 +01:00
Thomas Karpiniec ffa67ac1ae
[docs] Include link to a live instance in README (#3549) 2024-11-19 15:37:32 +00:00
38 changed files with 940 additions and 206 deletions

View file

@ -17,7 +17,7 @@ Documentation is at [docs.gotosocial.org](https://docs.gotosocial.org). You can
To build from source, check the [CONTRIBUTING.md](https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md) file.
Here's a screenshot of the instance landing page!
Here's a screenshot of the instance landing page! Check out the project's [official account](https://gts.superseriousbusiness.org/@gotosocial) running on GoToSocial.
![Screenshot of the landing page for the GoToSocial instance goblin.technology. It shows basic information about the instance; number of users and posts etc.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/overrides/public/instancesplash.png)
<!--overview-end-->

View file

@ -1138,12 +1138,12 @@ definitions:
type: boolean
x-go-name: AsDraft
content_type:
description: MIME content type to expect at URI.
description: MIME content type to use when parsing the permissions list.
example: text/csv
type: string
x-go-name: ContentType
count:
description: Count of domain permission entries discovered at URI.
description: Count of domain permission entries discovered at URI on last (successful) fetch.
example: 53
format: uint64
readOnly: true
@ -1188,6 +1188,11 @@ definitions:
example: block
type: string
x-go-name: PermissionType
title:
description: Title of this list, as set by admin who created or updated it.f
example: really cool list of neato pals
type: string
x-go-name: Title
uri:
description: URI to call in order to fetch the permissions list.
example: https://www.example.org/blocklists/list1.csv
@ -4993,7 +4998,7 @@ paths:
- description: The code to use for the emoji, which will be used by instance denizens to select it. This must be unique on the instance.
in: formData
name: shortcode
pattern: \w{2,30}
pattern: \w{1,30}
required: true
type: string
- description: A png or gif image of the emoji. Animated pngs work too! To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default.
@ -5139,7 +5144,7 @@ paths:
- description: The code to use for the emoji, which will be used by instance denizens to select it. This must be unique on the instance. Works for the `copy` action type only.
in: formData
name: shortcode
pattern: \w{2,30}
pattern: \w{1,30}
type: string
- description: A new png or gif image to use for the emoji. Animated pngs work too! To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default. Works for LOCAL emojis only.
in: formData
@ -5859,9 +5864,9 @@ paths:
required: true
type: string
- default: false
description: When removing the domain permission draft, also create a domain ignore entry for the target domain, so that drafts will not be created for this domain in the future.
description: When removing the domain permission draft, also create a domain exclude entry for the target domain, so that drafts will not be created for this domain in the future.
in: formData
name: ignore_target
name: exclude_target
type: boolean
produces:
- application/json
@ -5888,6 +5893,182 @@ paths:
summary: Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
tags:
- admin
/api/v1/admin/domain_permission_excludes:
get:
description: |-
The excludes will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
The next and previous queries can be parsed from the returned Link header.
Example:
```
<https://example.org/api/v1/admin/domain_permission_excludes?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_excludes?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
````
operationId: domainPermissionExcludesGet
parameters:
- description: Return only excludes that target the given domain.
in: query
name: domain
type: string
- description: Return only items *OLDER* than the given max ID (for paging downwards). The item with the specified ID will not be included in the response.
in: query
name: max_id
type: string
- description: Return only items *NEWER* than the given since ID. The item with the specified ID will not be included in the response.
in: query
name: since_id
type: string
- description: Return only items immediately *NEWER* than the given min ID (for paging upwards). The item with the specified ID will not be included in the response.
in: query
name: min_id
type: string
- default: 20
description: Number of items to return.
in: query
maximum: 100
minimum: 1
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: Domain permission excludes.
headers:
Link:
description: Links to the next and previous queries.
type: string
schema:
items:
$ref: '#/definitions/domainPermission'
type: array
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: View domain permission excludes.
tags:
- admin
post:
consumes:
- multipart/form-data
- application/json
description: |-
Excluded domains (and their subdomains) will not be automatically blocked or allowed when a list of domain permissions is imported or subscribed to.
You can still manually create domain blocks or domain allows for excluded domains, and any new or existing domain blocks or domain allows for an excluded domain will still be enforced.
operationId: domainPermissionExcludeCreate
parameters:
- description: Domain to create the permission exclude for.
in: formData
name: domain
type: string
- description: Private comment about this domain exclude.
in: formData
name: private_comment
type: string
produces:
- application/json
responses:
"200":
description: The newly created domain permission exclude.
schema:
$ref: '#/definitions/domainPermission'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"406":
description: not acceptable
"409":
description: conflict
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Create a domain permission exclude with the given parameters.
tags:
- admin
/api/v1/admin/domain_permission_excludes/{id}:
delete:
operationId: domainPermissionExcludeDelete
parameters:
- description: ID of the domain permission exclude.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: The removed domain permission exclude.
schema:
$ref: '#/definitions/domainPermission'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"406":
description: not acceptable
"409":
description: conflict
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Remove a domain permission exclude.
tags:
- admin
get:
operationId: domainPermissionExcludeGet
parameters:
- description: ID of the domain permission exclude.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Domain permission exclude.
schema:
$ref: '#/definitions/domainPermission'
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Get domain permission exclude with the given ID.
tags:
- admin
/api/v1/admin/email/test:
post:
consumes:

View file

@ -53,7 +53,7 @@
// The code to use for the emoji, which will be used by instance denizens to select it.
// This must be unique on the instance.
// type: string
// pattern: \w{2,30}
// pattern: \w{1,30}
// required: true
// -
// name: image

View file

@ -85,7 +85,7 @@
// The code to use for the emoji, which will be used by instance denizens to select it.
// This must be unique on the instance. Works for the `copy` action type only.
// type: string
// pattern: \w{2,30}
// pattern: \w{1,30}
// -
// name: image
// in: formData

View file

@ -560,7 +560,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
b, err := io.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only"}`, string(b))
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 1 and 30 characters, letters, numbers, and underscores only"}`, string(b))
}
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() {

View file

@ -155,7 +155,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -296,7 +296,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -437,7 +437,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -629,7 +629,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -792,7 +792,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
@ -974,7 +974,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",

View file

@ -66,50 +66,6 @@ type DomainPermission struct {
PermissionType string `json:"permission_type,omitempty"`
}
// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
//
// swagger:model domainPermissionSubscription
type DomainPermissionSubscription struct {
// The ID of the domain permission subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
ID string `json:"id"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType string `json:"permission_type"`
// If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft bool `json:"as_draft"`
// ID of the account that created this subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
CreatedByAccountID string `json:"created_by_account_id"`
// MIME content type to expect at URI.
// example: text/csv
ContentType string `json:"content_type"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI string `json:"uri"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername string `json:"fetch_username"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword string `json:"fetch_password"`
// Time at which the most recent fetch was attempted (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
// readonly: true
FetchedAt string `json:"fetched_at"`
// If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
// example: Oopsie doopsie, we made a fucky wucky.
// readonly: true
Error string `json:"error"`
// Count of domain permission entries discovered at URI.
// example: 53
// readonly: true
Count uint64 `json:"count"`
}
// DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
//
// swagger:ignore
@ -143,3 +99,77 @@ type DomainKeysExpireRequest struct {
// hostname/domain to expire keys for.
Domain string `form:"domain" json:"domain"`
}
// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
//
// swagger:model domainPermissionSubscription
type DomainPermissionSubscription struct {
// The ID of the domain permission subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
ID string `json:"id"`
// Title of this list, as set by admin who created or updated it.f
// example: really cool list of neato pals
Title string `json:"title"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType string `json:"permission_type"`
// If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft bool `json:"as_draft"`
// ID of the account that created this subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
CreatedByAccountID string `json:"created_by_account_id"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI string `json:"uri"`
// MIME content type to use when parsing the permissions list.
// example: text/csv
ContentType string `json:"content_type"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername string `json:"fetch_username,omitempty"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword string `json:"fetch_password,omitempty"`
// Time at which the most recent fetch was attempted (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
// readonly: true
FetchedAt string `json:"fetched_at,omitempty"`
// If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
// example: Oopsie doopsie, we made a fucky wucky.
// readonly: true
Error string `json:"error,omitempty"`
// Count of domain permission entries discovered at URI on last (successful) fetch.
// example: 53
// readonly: true
Count uint64 `json:"count"`
}
// DomainPermissionSubscriptionRequest represents a request to create or update a domain permission subscription..
//
// swagger:ignore
type DomainPermissionSubscriptionRequest struct {
// Title of this list, as set by admin who created or updated it.f
// example: really cool list of neato pals
Title string `form:"title" json:"title"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType string `form:"permission_type" json:"permission_type"`
// If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft bool `form:"as_draft" json:"as_draft"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI string `form:"uri" json:"uri"`
// MIME content type to use when parsing the permissions list.
// example: text/csv
ContentType string `form:"content_type" json:"content_type"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername string `form:"fetch_username" json:"fetch_username"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword string `form:"fetch_password" json:"fetch_password"`
}

View file

@ -0,0 +1,120 @@
// 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"
"errors"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type DomainPermissionDraftTestSuite struct {
BunDBStandardTestSuite
}
func (suite *DomainPermissionDraftTestSuite) TestPermDraftCreateGetDelete() {
var (
ctx = context.Background()
draft = &gtsmodel.DomainPermissionDraft{
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
PermissionType: gtsmodel.DomainPermissionBlock,
Domain: "exämple.org",
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
PrivateComment: "this domain is poo",
PublicComment: "this domain is poo, but phrased in a more outward-facing way",
Obfuscate: util.Ptr(false),
SubscriptionID: "01JCZN8PG55KKEVTDAY52D0T3P",
}
)
// Whack the draft in.
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
suite.FailNow(err.Error())
}
// Get the draft again.
dbDraft, err := suite.state.DB.GetDomainPermissionDraftByID(ctx, draft.ID)
if err != nil {
suite.FailNow(err.Error())
}
// Domain should have been stored punycoded.
suite.Equal("xn--exmple-cua.org", dbDraft.Domain)
// Search for domain using both
// punycode and unicode variants.
search1, err := suite.state.DB.GetDomainPermissionDrafts(
ctx,
gtsmodel.DomainPermissionUnknown,
"",
"exämple.org",
nil,
)
if err != nil {
suite.FailNow(err.Error())
}
if len(search1) != 1 {
suite.FailNow("couldn't get domain perm draft exämple.org")
}
search2, err := suite.state.DB.GetDomainPermissionDrafts(
ctx,
gtsmodel.DomainPermissionUnknown,
"",
"xn--exmple-cua.org",
nil,
)
if err != nil {
suite.FailNow(err.Error())
}
if len(search2) != 1 {
suite.FailNow("couldn't get domain perm draft example.org")
}
// Change ID + try to put the same draft again.
draft.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
err = suite.state.DB.PutDomainPermissionDraft(ctx, draft)
if !errors.Is(err, db.ErrAlreadyExists) {
suite.FailNow("was able to insert same domain perm draft twice")
}
// Put same draft but change permission type, should work.
draft.PermissionType = gtsmodel.DomainPermissionAllow
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
suite.FailNow(err.Error())
}
// Delete both drafts.
for _, id := range []string{
"01JCZN614XG85GCGAMSV9ZZAEJ",
"01JCZNVYSDT3JE385FABMJ7ADQ",
} {
if err := suite.state.DB.DeleteDomainPermissionDraft(ctx, id); err != nil {
suite.FailNow("error deleting domain permission draft")
}
}
}
func TestDomainPermissionDraftTestSuite(t *testing.T) {
suite.Run(t, new(DomainPermissionDraftTestSuite))
}

View file

@ -22,7 +22,6 @@
"errors"
"slices"
"github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
@ -65,14 +64,6 @@ func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string
return false, err
}
// Check if our host and given domain are equal
// or part of the same second-level domain; we
// always exclude such perms as creating blocks
// or allows in such cases may break things.
if dns.CompareDomainName(domain, config.GetHost()) >= 2 {
return true, nil
}
// Func to scan list of all
// excluded domain perms from DB.
loadF := func() ([]string, error) {
@ -80,12 +71,16 @@ func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string
if err := d.db.
NewSelect().
Table("domain_excludes").
Table("domain_permission_excludes").
Column("domain").
Scan(ctx, &domains); err != nil {
return nil, err
}
// Exclude our own domain as creating blocks
// or allows for self will likely break things.
domains = append(domains, config.GetHost())
return domains, nil
}

View file

@ -0,0 +1,185 @@
// 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"
"errors"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type DomainPermissionExcludeTestSuite struct {
BunDBStandardTestSuite
}
func (suite *DomainPermissionExcludeTestSuite) TestPermExcludeCreateGetDelete() {
var (
ctx = context.Background()
exclude = &gtsmodel.DomainPermissionExclude{
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
Domain: "exämple.org",
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
PrivateComment: "this domain is poo",
}
)
// Whack the exclude in.
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
suite.FailNow(err.Error())
}
// Get the exclude again.
dbExclude, err := suite.state.DB.GetDomainPermissionExcludeByID(ctx, exclude.ID)
if err != nil {
suite.FailNow(err.Error())
}
// Domain should have been stored punycoded.
suite.Equal("xn--exmple-cua.org", dbExclude.Domain)
// Search for domain using both
// punycode and unicode variants.
search1, err := suite.state.DB.GetDomainPermissionExcludes(
ctx,
"exämple.org",
nil,
)
if err != nil {
suite.FailNow(err.Error())
}
if len(search1) != 1 {
suite.FailNow("couldn't get domain perm exclude exämple.org")
}
search2, err := suite.state.DB.GetDomainPermissionExcludes(
ctx,
"xn--exmple-cua.org",
nil,
)
if err != nil {
suite.FailNow(err.Error())
}
if len(search2) != 1 {
suite.FailNow("couldn't get domain perm exclude example.org")
}
// Change ID + try to put the same exclude again.
exclude.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
err = suite.state.DB.PutDomainPermissionExclude(ctx, exclude)
if !errors.Is(err, db.ErrAlreadyExists) {
suite.FailNow("was able to insert same domain perm exclude twice")
}
// Delete both excludes.
for _, id := range []string{
"01JCZN614XG85GCGAMSV9ZZAEJ",
"01JCZNVYSDT3JE385FABMJ7ADQ",
} {
if err := suite.state.DB.DeleteDomainPermissionExclude(ctx, id); err != nil {
suite.FailNow("error deleting domain permission exclude")
}
}
}
func (suite *DomainPermissionExcludeTestSuite) TestExcluded() {
var (
ctx = context.Background()
createdByAccountID = suite.testAccounts["admin_account"].ID
)
// Insert some excludes into the db.
for _, exclude := range []*gtsmodel.DomainPermissionExclude{
{
ID: "01JD7AFFBBZSPY8R2M0JCGQGPW",
Domain: "example.org",
CreatedByAccountID: createdByAccountID,
},
{
ID: "01JD7AMK98E2QX78KXEZJ1RF5Z",
Domain: "boobs.com",
CreatedByAccountID: createdByAccountID,
},
{
ID: "01JD7AMXW3R3W98E91R62ACDA0",
Domain: "rad.boobs.com",
CreatedByAccountID: createdByAccountID,
},
{
ID: "01JD7AYYN5TXQVASB30PT08CE1",
Domain: "honkers.org",
CreatedByAccountID: createdByAccountID,
},
} {
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
suite.FailNow(err.Error())
}
}
type testCase struct {
domain string
excluded bool
}
for i, testCase := range []testCase{
{
domain: config.GetHost(),
excluded: true,
},
{
domain: "test.example.org",
excluded: true,
},
{
domain: "example.org",
excluded: true,
},
{
domain: "boobs.com",
excluded: true,
},
{
domain: "rad.boobs.com",
excluded: true,
},
{
domain: "sir.not.appearing.in.this.list",
excluded: false,
},
} {
excluded, err := suite.state.DB.IsDomainPermissionExcluded(ctx, testCase.domain)
if err != nil {
suite.FailNow(err.Error())
}
if excluded != testCase.excluded {
suite.Failf("",
"test %d: %s excluded should be %t",
i, testCase.domain, testCase.excluded,
)
}
}
}
func TestDomainPermissionExcludeTestSuite(t *testing.T) {
suite.Run(t, new(DomainPermissionExcludeTestSuite))
}

View file

@ -103,6 +103,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
// Ignore statuses that are currently pending approval.
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
// Ignore statuses that are direct messages.
q = q.Where("NOT ? = ?", bun.Ident("status.visibility"), "direct")
count, err := q.Count(ctx)
if err != nil {
return 0, err

View file

@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
suite.NoError(err)
suite.Equal(20, count)
suite.Equal(19, count)
}
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {

View file

@ -0,0 +1,82 @@
// 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"
"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 {
// Create `domain_permission_drafts`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionDraft)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create `domain_permission_ignores`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionExclude)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create indexes. Indices. Indie sexes.
for table, indexes := range map[string]map[string][]string{
"domain_permission_drafts": {
"domain_permission_drafts_domain_idx": {"domain"},
"domain_permission_drafts_subscription_id_idx": {"subscription_id"},
},
} {
for index, columns := range indexes {
if _, err := tx.
NewCreateIndex().
Table(table).
Index(index).
Column(columns...).
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

@ -27,16 +27,6 @@
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Create `domain_permission_drafts`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionDraft)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create `domain_permission_subscriptions`.
if _, err := tx.
NewCreateTable().
@ -46,21 +36,8 @@ func init() {
return err
}
// Create `domain_permission_ignores`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionExclude)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create indexes. Indices. Indie sexes.
for table, indexes := range map[string]map[string][]string{
"domain_permission_drafts": {
"domain_permission_drafts_domain_idx": {"domain"},
"domain_permission_drafts_subscription_id_idx": {"subscription_id"},
},
"domain_permission_subscriptions": {
"domain_permission_subscriptions_permission_type_idx": {"permission_type"},
},

View file

@ -130,6 +130,9 @@ type Domain interface {
// DeleteDomainPermissionExclude deletes one DomainPermissionExclude with the given id.
DeleteDomainPermissionExclude(ctx context.Context, id string) error
// IsDomainPermissionExcluded returns true if the given domain matches in the list of excluded domains.
IsDomainPermissionExcluded(ctx context.Context, domain string) (bool, error)
/*
Domain permission subscription stuff.
*/

View file

@ -23,12 +23,12 @@ type DomainPermissionSubscription struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Time when this item was created.
Title string `bun:",nullzero"` // Moderator-set title for this list.
PermissionType DomainPermissionType `bun:",notnull"` // Permission type of the subscription.
PermissionType DomainPermissionType `bun:",nullzero,notnull"` // Permission type of the subscription.
AsDraft *bool `bun:",nullzero,notnull,default:true"` // Create domain permission entries resulting from this subscription as drafts.
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
ContentType string `bun:",nullzero,notnull"` // Content type to expect from the URI.
URI string `bun:",unique,nullzero,notnull"` // URI of the domain permission list.
ContentType DomainPermSubContentType `bun:",nullzero,notnull"` // Content type to expect from the URI.
FetchUsername string `bun:",nullzero"` // Username to send when doing a GET of URI using basic auth.
FetchPassword string `bun:",nullzero"` // Password to send when doing a GET of URI using basic auth.
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when fetch of URI was last attempted.
@ -36,3 +36,38 @@ type DomainPermissionSubscription struct {
Error string `bun:",nullzero"` // If IsError=true, this field contains the error resulting from the attempted fetch.
Count uint64 `bun:""` // Count of domain permission entries discovered at URI.
}
type DomainPermSubContentType uint8
const (
DomainPermSubContentTypeUnknown DomainPermSubContentType = iota
DomainPermSubContentTypeCSV // text/csv
DomainPermSubContentTypeJSON // application/json
DomainPermSubContentTypePlain // text/plain
)
func (p DomainPermSubContentType) String() string {
switch p {
case DomainPermSubContentTypeCSV:
return "text/csv"
case DomainPermSubContentTypeJSON:
return "application/json"
case DomainPermSubContentTypePlain:
return "text/plain"
default:
return "unknown"
}
}
func NewDomainPermSubContentType(in string) DomainPermSubContentType {
switch in {
case "text/csv":
return DomainPermSubContentTypeCSV
case "application/json":
return DomainPermSubContentTypeCSV
case "text/plain":
return DomainPermSubContentTypeCSV
default:
return DomainPermSubContentTypeUnknown
}
}

View file

@ -249,7 +249,8 @@ func (p *Processor) DomainPermissionDraftAccept(
deleteDraft()
return new, actionID, errWithCode
} else {
}
// Domain permission exists but we should overwrite
// it by just updating the existing domain permission.
// Domain can't change, so no need to re-run side effects.
@ -260,7 +261,6 @@ func (p *Processor) DomainPermissionDraftAccept(
existing.SetObfuscate(permDraft.Obfuscate)
existing.SetSubscriptionID(permDraft.SubscriptionID)
var err error
switch dp := existing.(type) {
case *gtsmodel.DomainBlock:
err = p.state.DB.UpdateDomainBlock(ctx, dp)
@ -280,7 +280,6 @@ func (p *Processor) DomainPermissionDraftAccept(
apiPerm, errWithCode := p.apiDomainPerm(ctx, existing, false)
return apiPerm, "", errWithCode
}
}
func (p *Processor) DomainPermissionDraftRemove(

View file

@ -0,0 +1,31 @@
// 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 admin
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *Processor) DomainPermissionSubscriptionCreate(
ctx context.Context,
acct *gtsmodel.Account,
) {
}

View file

@ -46,7 +46,7 @@
domainGrp = `(?:` + alphaNumeric + `|\.|\-|\:)` // Non-capturing group that matches against a single valid domain character.
mentionName = `^@(` + usernameGrp + `+)(?:@(` + domainGrp + `+))?$` // Extract parts of one mention, maybe including domain.
mentionFinder = `(?:^|\s)(@` + usernameGrp + `+(?:@` + domainGrp + `+)?)` // Extract all mentions from a text, each mention may include domain.
emojiShortcode = `\w{2,30}` // Pattern for emoji shortcodes. maximumEmojiShortcodeLength = 30
emojiShortcode = `\w{1,30}` // Pattern for emoji shortcodes. maximumEmojiShortcodeLength = 30
emojiFinder = `(?:\b)?:(` + emojiShortcode + `):(?:\b)?` // Extract all emoji shortcodes from a text.
emojiValidator = `^` + emojiShortcode + `$` // Validate a single emoji shortcode.
usernameStrict = `^[a-z0-9_]{1,64}$` // Pattern for usernames on THIS instance. maximumUsernameLength = 64

View file

@ -1993,7 +1993,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",

View file

@ -190,11 +190,11 @@ func CustomCSS(customCSS string) error {
}
// EmojiShortcode just runs the given shortcode through the regular expression
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 2-30 characters,
// for emoji shortcodes, to figure out whether it's a valid shortcode, ie., 1-30 characters,
// a-zA-Z, numbers, and underscores.
func EmojiShortcode(shortcode string) error {
if !regexes.EmojiValidator.MatchString(shortcode) {
return fmt.Errorf("shortcode %s did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only", shortcode)
return fmt.Errorf("shortcode %s did not pass validation, must be between 1 and 30 characters, letters, numbers, and underscores only", shortcode)
}
return nil
}

View file

@ -345,7 +345,7 @@ type testStruct struct {
},
{
shortcode: "p",
ok: false,
ok: true,
},
{
shortcode: "pp",
@ -361,6 +361,10 @@ type testStruct struct {
},
{
shortcode: "_",
ok: true,
},
{
shortcode: "",
ok: false,
},
{

View file

@ -34,6 +34,7 @@ EXPECT=$(cat << "EOF"
"client-mem-ratio": 0.1,
"conversation-last-status-ids-mem-ratio": 2,
"conversation-mem-ratio": 1,
"domain-permission-draft-mem-ratio": 0.5,
"emoji-category-mem-ratio": 0.1,
"emoji-mem-ratio": 3,
"filter-keyword-mem-ratio": 0.5,

View file

@ -1403,7 +1403,8 @@ button.tab-button {
}
}
.domain-permission-draft-details {
.domain-permission-draft-details,
.domain-permission-exclude-details {
.info-list {
margin-top: 1rem;
}

View file

@ -119,7 +119,7 @@ export default function NewEmojiForm() {
label="Shortcode, must be unique among the instance's local emoji"
autoCapitalize="none"
spellCheck="false"
{...{pattern: "^\\w{2,30}$"}}
{...{pattern: "^\\w{1,30}$"}}
/>
<CategorySelect

View file

@ -22,7 +22,7 @@ import { useMemo } from "react";
import { useTextInput } from "../../../../lib/form";
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
const shortcodeRegex = /^\w{2,30}$/;
const shortcodeRegex = /^\w{1,30}$/;
export default function useShortcode() {
const { data: emoji = [] } = useListEmojiQuery({
@ -42,8 +42,8 @@ export default function useShortcode() {
return "Shortcode already in use";
}
if (code.length < 2 || code.length > 30) {
return "Shortcode must be between 2 and 30 characters";
if (code.length < 1 || code.length > 30) {
return "Shortcode must be between 1 and 30 characters";
}
if (!shortcodeRegex.test(code)) {

View file

@ -0,0 +1,43 @@
/*
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/>.
*/
import React from "react";
export function DomainPermissionDraftHelpText() {
return (
<>
Domain permission drafts are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a draft.
</>
);
}
export function DomainPermissionDraftDocsLink() {
return (
<a
href="https://docs.gotosocial.org/en/latest/admin/settings/#domain-permission-drafts"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about domain permission drafts (opens in a new tab)
</a>
);
}

View file

@ -29,6 +29,7 @@ import { Error as ErrorC } from "../../../../components/error";
import { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
import { useCapitalize } from "../../../../lib/util";
import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common";
export default function DomainPermissionDraftsSearch() {
return (
@ -38,10 +39,9 @@ export default function DomainPermissionDraftsSearch() {
<p>
You can use the form below to search through domain permission drafts.
<br/>
Domain permission drafts are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a draft.
<DomainPermissionDraftHelpText />
</p>
<DomainPermissionDraftDocsLink />
</div>
<DomainPermissionDraftsSearchForm />
</div>

View file

@ -25,6 +25,7 @@ import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button";
import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs";
import { useLocation } from "wouter";
import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common";
export default function DomainPermissionDraftNew() {
const [ _location, setLocation ] = useLocation();
@ -67,13 +68,8 @@ export default function DomainPermissionDraftNew() {
>
<div className="form-section-docs">
<h2>New Domain Permission Draft</h2>
<p>
You can use the form below to create a new domain permission draft.
<br/>
Domain permission drafts are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a draft.
</p>
<p><DomainPermissionDraftHelpText /></p>
<DomainPermissionDraftDocsLink />
</div>
<RadioGroup

View file

@ -0,0 +1,54 @@
/*
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/>.
*/
import React from "react";
export function DomainPermissionExcludeHelpText() {
return (
<>
Domain permission excludes prevent permissions for a domain (and all
subdomains) from being auomatically managed by domain permission subscriptions.
<br/>
For example, if you create an exclude entry for <code>example.org</code>, then
a blocklist or allowlist subscription will <em>exclude</em> entries for <code>example.org</code>
and any of its subdomains (<code>sub.example.org</code>, <code>another.sub.example.org</code> etc.)
when creating domain permission drafts and domain blocks/allows.
<br/>
This functionality allows you to manually manage permissions for excluded domains,
in cases where you know you definitely do or don't want to federate with a given domain,
no matter what entries are contained in a domain permission subscription.
<br/>
Note that by itself, creation of an exclude entry for a given domain does not affect
federation with that domain at all, it is only useful in combination with permission subscriptions.
</>
);
}
export function DomainPermissionExcludeDocsLink() {
return (
<a
href="https://docs.gotosocial.org/en/latest/admin/settings/#domain-permission-excludes"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about domain permission excludes (opens in a new tab)
</a>
);
}

View file

@ -18,20 +18,21 @@
*/
import React from "react";
import { useParams } from "wouter";
import { useLocation, useParams } from "wouter";
import Loading from "../../../../components/loading";
import { useBaseUrl } from "../../../../lib/navigation/util";
import BackButton from "../../../../components/back-button";
import { Error as ErrorC } from "../../../../components/error";
import UsernameLozenge from "../../../../components/username-lozenge";
import { useGetDomainPermissionExcludeQuery } from "../../../../lib/query/admin/domain-permissions/excludes";
import { useDeleteDomainPermissionExcludeMutation, useGetDomainPermissionExcludeQuery } from "../../../../lib/query/admin/domain-permissions/excludes";
import MutationButton from "../../../../components/form/mutation-button";
export default function DomainPermissionExcludeDetail() {
const baseUrl = useBaseUrl();
const backLocation: string = history.state?.backLocation ?? `~${baseUrl}`;
const params = useParams();
let id = params.permExcludeId as string | undefined;
const params = useParams();
let id = params.excludeId as string | undefined;
if (!id) {
throw "no perm ID";
}
@ -54,13 +55,7 @@ export default function DomainPermissionExcludeDetail() {
const created = permExclude.created_at ? new Date(permExclude.created_at).toDateString(): "unknown";
const domain = permExclude.domain;
const permType = permExclude.permission_type;
if (!permType) {
return <ErrorC error={new Error("permission_type was undefined")} />;
}
const publicComment = permExclude.public_comment ?? "[none]";
const privateComment = permExclude.private_comment ?? "[none]";
const subscriptionID = permExclude.subscription_id ?? "[none]";
return (
<div className="domain-permission-exclude-details">
@ -84,28 +79,10 @@ export default function DomainPermissionExcludeDetail() {
<dt>Domain</dt>
<dd>{domain}</dd>
</div>
<div className="info-list-entry">
<dt>Permission type</dt>
<dd className={`permission-type ${permType}`}>
<i
aria-hidden={true}
className={`fa fa-${permType === "allow" ? "check" : "close"}`}
></i>
{permType}
</dd>
</div>
<div className="info-list-entry">
<dt>Private comment</dt>
<dd>{privateComment}</dd>
</div>
<div className="info-list-entry">
<dt>Public comment</dt>
<dd>{publicComment}</dd>
</div>
<div className="info-list-entry">
<dt>Subscription ID</dt>
<dd>{subscriptionID}</dd>
</div>
</dl>
<HandleExclude
id={id}
@ -115,6 +92,28 @@ export default function DomainPermissionExcludeDetail() {
);
}
function HandleExclude({ id, backLocation }: { id: string, backLocation: string }) {
return <></>;
function HandleExclude({ id, backLocation}: {id: string, backLocation: string}) {
const [_location, setLocation] = useLocation();
const [ deleteExclude, deleteResult ] = useDeleteDomainPermissionExcludeMutation();
return (
<MutationButton
label={`Delete exclude`}
title={`Delete exclude`}
type="button"
className="button danger"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
deleteExclude(id).then(res => {
if ("data" in res) {
setLocation(backLocation);
}
});
}}
disabled={false}
showError={true}
result={deleteResult}
/>
);
}

View file

@ -28,6 +28,7 @@ import { DomainPerm } from "../../../../lib/types/domain-permission";
import { Error as ErrorC } from "../../../../components/error";
import { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
import { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
export default function DomainPermissionExcludesSearch() {
return (
@ -37,10 +38,9 @@ export default function DomainPermissionExcludesSearch() {
<p>
You can use the form below to search through domain permission excludes.
<br/>
Domain permission excludes are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a exclude.
<DomainPermissionExcludeHelpText />
</p>
<DomainPermissionExcludeDocsLink />
</div>
<DomainPermissionExcludesSearchForm />
</div>
@ -204,7 +204,6 @@ function ExcludeListEntry({ permExclude, linkTo, backLocation }: ExcludeEntryPro
role="link"
tabIndex={0}
>
<h3>{`Exclude ${domain}`}</h3>
<dl className="info-list">
<div className="info-list-entry">
<dt>Domain:</dt>

View file

@ -25,6 +25,7 @@ import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button";
import { TextArea, TextInput } from "../../../../components/form/inputs";
import { useLocation } from "wouter";
import { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
export default function DomainPermissionExcludeNew() {
const [ _location, setLocation ] = useLocation();
@ -59,13 +60,8 @@ export default function DomainPermissionExcludeNew() {
>
<div className="form-section-docs">
<h2>New Domain Permission Exclude</h2>
<p>
You can use the form below to create a new domain permission exclude.
<br/>
Domain permission excludes are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a exclude.
</p>
<p><DomainPermissionExcludeHelpText /></p>
<DomainPermissionExcludeDocsLink />
</div>
<TextInput
@ -79,7 +75,7 @@ export default function DomainPermissionExcludeNew() {
<TextArea
field={form.private_comment}
label={"Private comment"}
placeholder="This domain is like unto a clown car full of clowns, I suggest we block it forthwith."
placeholder="Created an exclude for this domain because we should manage it manually."
autoCapitalize="sentences"
rows={3}
/>

View file

@ -2922,9 +2922,9 @@ create-require@^1.1.0:
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"