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. 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) ![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--> <!--overview-end-->

View file

@ -1138,12 +1138,12 @@ definitions:
type: boolean type: boolean
x-go-name: AsDraft x-go-name: AsDraft
content_type: content_type:
description: MIME content type to expect at URI. description: MIME content type to use when parsing the permissions list.
example: text/csv example: text/csv
type: string type: string
x-go-name: ContentType x-go-name: ContentType
count: 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 example: 53
format: uint64 format: uint64
readOnly: true readOnly: true
@ -1188,6 +1188,11 @@ definitions:
example: block example: block
type: string type: string
x-go-name: PermissionType 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: uri:
description: URI to call in order to fetch the permissions list. description: URI to call in order to fetch the permissions list.
example: https://www.example.org/blocklists/list1.csv 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. - 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 in: formData
name: shortcode name: shortcode
pattern: \w{2,30} pattern: \w{1,30}
required: true required: true
type: string 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. - 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. - 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 in: formData
name: shortcode name: shortcode
pattern: \w{2,30} pattern: \w{1,30}
type: string 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. - 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 in: formData
@ -5859,9 +5864,9 @@ paths:
required: true required: true
type: string type: string
- default: false - 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 in: formData
name: ignore_target name: exclude_target
type: boolean type: boolean
produces: produces:
- application/json - application/json
@ -5888,6 +5893,182 @@ paths:
summary: Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain. summary: Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
tags: tags:
- admin - 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: /api/v1/admin/email/test:
post: post:
consumes: consumes:

View file

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

View file

@ -560,7 +560,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
b, err := io.ReadAll(result.Body) b, err := io.ReadAll(result.Body)
suite.NoError(err) 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() { func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() {

View file

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

View file

@ -66,50 +66,6 @@ type DomainPermission struct {
PermissionType string `json:"permission_type,omitempty"` 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). // DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
// //
// swagger:ignore // swagger:ignore
@ -143,3 +99,77 @@ type DomainKeysExpireRequest struct {
// hostname/domain to expire keys for. // hostname/domain to expire keys for.
Domain string `form:"domain" json:"domain"` 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" "errors"
"slices" "slices"
"github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
@ -65,14 +64,6 @@ func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string
return false, err 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 // Func to scan list of all
// excluded domain perms from DB. // excluded domain perms from DB.
loadF := func() ([]string, error) { loadF := func() ([]string, error) {
@ -80,12 +71,16 @@ func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string
if err := d.db. if err := d.db.
NewSelect(). NewSelect().
Table("domain_excludes"). Table("domain_permission_excludes").
Column("domain"). Column("domain").
Scan(ctx, &domains); err != nil { Scan(ctx, &domains); err != nil {
return nil, err 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 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. // Ignore statuses that are currently pending approval.
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true) 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) count, err := q.Count(ctx)
if err != nil { if err != nil {
return 0, err return 0, err

View file

@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
func (suite *InstanceTestSuite) TestCountInstanceStatuses() { func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost()) count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
suite.NoError(err) suite.NoError(err)
suite.Equal(20, count) suite.Equal(19, count)
} }
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() { 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() { func init() {
up := func(ctx context.Context, db *bun.DB) error { up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) 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`. // Create `domain_permission_subscriptions`.
if _, err := tx. if _, err := tx.
NewCreateTable(). NewCreateTable().
@ -46,21 +36,8 @@ func init() {
return err 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. // Create indexes. Indices. Indie sexes.
for table, indexes := range map[string]map[string][]string{ 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": {
"domain_permission_subscriptions_permission_type_idx": {"permission_type"}, "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 deletes one DomainPermissionExclude with the given id.
DeleteDomainPermissionExclude(ctx context.Context, id string) error 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. Domain permission subscription stuff.
*/ */

View file

@ -20,19 +20,54 @@
import "time" import "time"
type DomainPermissionSubscription struct { type DomainPermissionSubscription struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. 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. 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. 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. 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. CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID. 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.
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. 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. 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. FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when fetch of URI was last attempted.
IsError *bool `bun:",nullzero,notnull,default:false"` // True if last fetch attempt of URI resulted in an error. IsError *bool `bun:",nullzero,notnull,default:false"` // True if last fetch attempt of URI resulted in an error.
Error string `bun:",nullzero"` // If IsError=true, this field contains the error resulting from the attempted fetch. 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. 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,38 +249,37 @@ func (p *Processor) DomainPermissionDraftAccept(
deleteDraft() deleteDraft()
return new, actionID, errWithCode 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.
existing.SetCreatedByAccountID(permDraft.CreatedByAccountID)
existing.SetCreatedByAccount(permDraft.CreatedByAccount)
existing.SetPrivateComment(permDraft.PrivateComment)
existing.SetPublicComment(permDraft.PublicComment)
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)
case *gtsmodel.DomainAllow:
err = p.state.DB.UpdateDomainAllow(ctx, dp)
}
if err != nil {
err := gtserror.Newf("db error updating existing domain permission: %w", err)
return nil, "", gtserror.NewErrorInternalError(err)
}
// Clean up the draft
// before returning.
deleteDraft()
apiPerm, errWithCode := p.apiDomainPerm(ctx, existing, false)
return apiPerm, "", errWithCode
} }
// 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.
existing.SetCreatedByAccountID(permDraft.CreatedByAccountID)
existing.SetCreatedByAccount(permDraft.CreatedByAccount)
existing.SetPrivateComment(permDraft.PrivateComment)
existing.SetPublicComment(permDraft.PublicComment)
existing.SetObfuscate(permDraft.Obfuscate)
existing.SetSubscriptionID(permDraft.SubscriptionID)
switch dp := existing.(type) {
case *gtsmodel.DomainBlock:
err = p.state.DB.UpdateDomainBlock(ctx, dp)
case *gtsmodel.DomainAllow:
err = p.state.DB.UpdateDomainAllow(ctx, dp)
}
if err != nil {
err := gtserror.Newf("db error updating existing domain permission: %w", err)
return nil, "", gtserror.NewErrorInternalError(err)
}
// Clean up the draft
// before returning.
deleteDraft()
apiPerm, errWithCode := p.apiDomainPerm(ctx, existing, false)
return apiPerm, "", errWithCode
} }
func (p *Processor) DomainPermissionDraftRemove( 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. domainGrp = `(?:` + alphaNumeric + `|\.|\-|\:)` // Non-capturing group that matches against a single valid domain character.
mentionName = `^@(` + usernameGrp + `+)(?:@(` + domainGrp + `+))?$` // Extract parts of one mention, maybe including domain. 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. 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. emojiFinder = `(?:\b)?:(` + emojiShortcode + `):(?:\b)?` // Extract all emoji shortcodes from a text.
emojiValidator = `^` + emojiShortcode + `$` // Validate a single emoji shortcode. emojiValidator = `^` + emojiShortcode + `$` // Validate a single emoji shortcode.
usernameStrict = `^[a-z0-9_]{1,64}$` // Pattern for usernames on THIS instance. maximumUsernameLength = 64 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": { "stats": {
"domain_count": 2, "domain_count": 2,
"status_count": 20, "status_count": 19,
"user_count": 4 "user_count": 4
}, },
"thumbnail": "http://localhost:8080/assets/logo.webp", "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 // 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. // a-zA-Z, numbers, and underscores.
func EmojiShortcode(shortcode string) error { func EmojiShortcode(shortcode string) error {
if !regexes.EmojiValidator.MatchString(shortcode) { 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 return nil
} }

View file

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

View file

@ -34,6 +34,7 @@ EXPECT=$(cat << "EOF"
"client-mem-ratio": 0.1, "client-mem-ratio": 0.1,
"conversation-last-status-ids-mem-ratio": 2, "conversation-last-status-ids-mem-ratio": 2,
"conversation-mem-ratio": 1, "conversation-mem-ratio": 1,
"domain-permission-draft-mem-ratio": 0.5,
"emoji-category-mem-ratio": 0.1, "emoji-category-mem-ratio": 0.1,
"emoji-mem-ratio": 3, "emoji-mem-ratio": 3,
"filter-keyword-mem-ratio": 0.5, "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 { .info-list {
margin-top: 1rem; margin-top: 1rem;
} }

View file

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

View file

@ -22,7 +22,7 @@ import { useMemo } from "react";
import { useTextInput } from "../../../../lib/form"; import { useTextInput } from "../../../../lib/form";
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji"; import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
const shortcodeRegex = /^\w{2,30}$/; const shortcodeRegex = /^\w{1,30}$/;
export default function useShortcode() { export default function useShortcode() {
const { data: emoji = [] } = useListEmojiQuery({ const { data: emoji = [] } = useListEmojiQuery({
@ -42,8 +42,8 @@ export default function useShortcode() {
return "Shortcode already in use"; return "Shortcode already in use";
} }
if (code.length < 2 || code.length > 30) { if (code.length < 1 || code.length > 30) {
return "Shortcode must be between 2 and 30 characters"; return "Shortcode must be between 1 and 30 characters";
} }
if (!shortcodeRegex.test(code)) { 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 { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators"; import { formDomainValidator } from "../../../../lib/util/formvalidators";
import { useCapitalize } from "../../../../lib/util"; import { useCapitalize } from "../../../../lib/util";
import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common";
export default function DomainPermissionDraftsSearch() { export default function DomainPermissionDraftsSearch() {
return ( return (
@ -38,10 +39,9 @@ export default function DomainPermissionDraftsSearch() {
<p> <p>
You can use the form below to search through domain permission drafts. You can use the form below to search through domain permission drafts.
<br/> <br/>
Domain permission drafts are domain block or domain allow entries that are not yet in force. <DomainPermissionDraftHelpText />
<br/>
You can choose to accept or remove a draft.
</p> </p>
<DomainPermissionDraftDocsLink />
</div> </div>
<DomainPermissionDraftsSearchForm /> <DomainPermissionDraftsSearchForm />
</div> </div>

View file

@ -25,6 +25,7 @@ import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button"; import MutationButton from "../../../../components/form/mutation-button";
import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs"; import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common";
export default function DomainPermissionDraftNew() { export default function DomainPermissionDraftNew() {
const [ _location, setLocation ] = useLocation(); const [ _location, setLocation ] = useLocation();
@ -67,13 +68,8 @@ export default function DomainPermissionDraftNew() {
> >
<div className="form-section-docs"> <div className="form-section-docs">
<h2>New Domain Permission Draft</h2> <h2>New Domain Permission Draft</h2>
<p> <p><DomainPermissionDraftHelpText /></p>
You can use the form below to create a new domain permission draft. <DomainPermissionDraftDocsLink />
<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>
</div> </div>
<RadioGroup <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 React from "react";
import { useParams } from "wouter"; import { useLocation, useParams } from "wouter";
import Loading from "../../../../components/loading"; import Loading from "../../../../components/loading";
import { useBaseUrl } from "../../../../lib/navigation/util"; import { useBaseUrl } from "../../../../lib/navigation/util";
import BackButton from "../../../../components/back-button"; import BackButton from "../../../../components/back-button";
import { Error as ErrorC } from "../../../../components/error"; import { Error as ErrorC } from "../../../../components/error";
import UsernameLozenge from "../../../../components/username-lozenge"; 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() { export default function DomainPermissionExcludeDetail() {
const baseUrl = useBaseUrl(); const baseUrl = useBaseUrl();
const backLocation: string = history.state?.backLocation ?? `~${baseUrl}`; 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) { if (!id) {
throw "no perm 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 created = permExclude.created_at ? new Date(permExclude.created_at).toDateString(): "unknown";
const domain = permExclude.domain; 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 privateComment = permExclude.private_comment ?? "[none]";
const subscriptionID = permExclude.subscription_id ?? "[none]";
return ( return (
<div className="domain-permission-exclude-details"> <div className="domain-permission-exclude-details">
@ -84,37 +79,41 @@ export default function DomainPermissionExcludeDetail() {
<dt>Domain</dt> <dt>Domain</dt>
<dd>{domain}</dd> <dd>{domain}</dd>
</div> </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"> <div className="info-list-entry">
<dt>Private comment</dt> <dt>Private comment</dt>
<dd>{privateComment}</dd> <dd>{privateComment}</dd>
</div> </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> </dl>
<HandleExclude <HandleExclude
id={id} id={id}
backLocation={backLocation} backLocation={backLocation}
/> />
</div> </div>
); );
} }
function HandleExclude({ id, backLocation }: { id: string, backLocation: string }) { function HandleExclude({ id, backLocation}: {id: string, backLocation: string}) {
return <></>; 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 { Error as ErrorC } from "../../../../components/error";
import { Select, TextInput } from "../../../../components/form/inputs"; import { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators"; import { formDomainValidator } from "../../../../lib/util/formvalidators";
import { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
export default function DomainPermissionExcludesSearch() { export default function DomainPermissionExcludesSearch() {
return ( return (
@ -37,10 +38,9 @@ export default function DomainPermissionExcludesSearch() {
<p> <p>
You can use the form below to search through domain permission excludes. You can use the form below to search through domain permission excludes.
<br/> <br/>
Domain permission excludes are domain block or domain allow entries that are not yet in force. <DomainPermissionExcludeHelpText />
<br/>
You can choose to accept or remove a exclude.
</p> </p>
<DomainPermissionExcludeDocsLink />
</div> </div>
<DomainPermissionExcludesSearchForm /> <DomainPermissionExcludesSearchForm />
</div> </div>
@ -204,7 +204,6 @@ function ExcludeListEntry({ permExclude, linkTo, backLocation }: ExcludeEntryPro
role="link" role="link"
tabIndex={0} tabIndex={0}
> >
<h3>{`Exclude ${domain}`}</h3>
<dl className="info-list"> <dl className="info-list">
<div className="info-list-entry"> <div className="info-list-entry">
<dt>Domain:</dt> <dt>Domain:</dt>

View file

@ -25,6 +25,7 @@ import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button"; import MutationButton from "../../../../components/form/mutation-button";
import { TextArea, TextInput } from "../../../../components/form/inputs"; import { TextArea, TextInput } from "../../../../components/form/inputs";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
export default function DomainPermissionExcludeNew() { export default function DomainPermissionExcludeNew() {
const [ _location, setLocation ] = useLocation(); const [ _location, setLocation ] = useLocation();
@ -59,13 +60,8 @@ export default function DomainPermissionExcludeNew() {
> >
<div className="form-section-docs"> <div className="form-section-docs">
<h2>New Domain Permission Exclude</h2> <h2>New Domain Permission Exclude</h2>
<p> <p><DomainPermissionExcludeHelpText /></p>
You can use the form below to create a new domain permission exclude. <DomainPermissionExcludeDocsLink />
<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>
</div> </div>
<TextInput <TextInput
@ -79,7 +75,7 @@ export default function DomainPermissionExcludeNew() {
<TextArea <TextArea
field={form.private_comment} field={form.private_comment}
label={"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" autoCapitalize="sentences"
rows={3} rows={3}
/> />

View file

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