mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-10 05:48:08 -06:00
[feature] Allow exposing allows, implement /api/v1/domain_blocks and /api/v1/domain_allows (#4169)
- adds config flags `instance-expose-allowlist` and `instance-expose-allowlist-web` to allow instance admins to expose their allowlist via the web + api. - renames `instance-expose-suspended` and `instance-expose-suspended-web` to `instance-expose-blocklist` and `instance-expose-blocklist-web`. - deprecates the `suspended` filter on `/api/v1/instance/peers` endpoint and adds `blocked` and `allowed` filters - adds the `flat` query param to `/api/v1/instance/peers` to allow forcing return of a flat list of domains - implements `/api/v1/instance/domain_blocks` and `/api/v1/instance/domain_allows` endpoints with or without auth depending on config - rejigs the instance about page to include a general section on domain permissions, with block and allow subsections (and appropriate links) Closes https://codeberg.org/superseriousbusiness/gotosocial/issues/3847 Closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4150 Prerequisite to https://codeberg.org/superseriousbusiness/gotosocial/issues/3711 Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4169 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
parent
3a29a59e55
commit
ec4d4d0115
23 changed files with 986 additions and 271 deletions
172
internal/api/client/instance/domainperms.go
Normal file
172
internal/api/client/instance/domainperms.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// 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 instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// InstanceDomainBlocksGETHandler swagger:operation GET /api/v1/instance/domain_blocks instanceDomainBlocksGet
|
||||
//
|
||||
// List blocked domains.
|
||||
//
|
||||
// OAuth token may need to be provided depending on setting `instance-expose-blocklist`.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - instance
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer: []
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: List of blocked domains.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domain"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) InstanceDomainBlocksGETHandler(c *gin.Context) {
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
false, false, false, false,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if (authed.Account == nil || authed.User == nil) && !config.GetInstanceExposeBlocklist() {
|
||||
const errText = "domain blocks endpoint requires an authenticated account/user"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
data, errWithCode := m.processor.InstancePeersGet(
|
||||
c.Request.Context(),
|
||||
true, // Include blocked.
|
||||
false, // Don't include allowed.
|
||||
false, // Don't include open.
|
||||
false, // Don't flatten.
|
||||
true, // Include severity.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, data)
|
||||
}
|
||||
|
||||
// InstanceDomainAllowsGETHandler swagger:operation GET /api/v1/instance/domain_allows instanceDomainAllowsGet
|
||||
//
|
||||
// List explicitly allowed domains.
|
||||
//
|
||||
// OAuth token may need to be provided depending on setting `instance-expose-allowlist`.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - instance
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer: []
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: List of explicitly allowed domains.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domain"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) InstanceDomainAllowsGETHandler(c *gin.Context) {
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
false, false, false, false,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if (authed.Account == nil || authed.User == nil) && !config.GetInstanceExposeAllowlist() {
|
||||
const errText = "domain allows endpoint requires an authenticated account/user"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
data, errWithCode := m.processor.InstancePeersGet(
|
||||
c.Request.Context(),
|
||||
false, // Don't include blocked.
|
||||
true, // Include allowed.
|
||||
false, // Don't include open.
|
||||
false, // Don't flatten.
|
||||
false, // Don't include severity.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, data)
|
||||
}
|
||||
|
|
@ -29,7 +29,10 @@ const (
|
|||
InstanceInformationPathV2 = "/v2/instance"
|
||||
InstancePeersPath = InstanceInformationPathV1 + "/peers"
|
||||
InstanceRulesPath = InstanceInformationPathV1 + "/rules"
|
||||
InstanceBlocklistPath = InstanceInformationPathV1 + "/domain_blocks"
|
||||
InstanceAllowlistPath = InstanceInformationPathV1 + "/domain_allows"
|
||||
PeersFilterKey = "filter" // PeersFilterKey is used to provide filters to /api/v1/instance/peers
|
||||
PeersFlatKey = "flat" // PeersFlatKey is used to set "flat=true" in /api/v1/instance/peers
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
|
@ -45,9 +48,9 @@ func New(processor *processing.Processor) *Module {
|
|||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, InstanceInformationPathV1, m.InstanceInformationGETHandlerV1)
|
||||
attachHandler(http.MethodGet, InstanceInformationPathV2, m.InstanceInformationGETHandlerV2)
|
||||
|
||||
attachHandler(http.MethodPatch, InstanceInformationPathV1, m.InstanceUpdatePATCHHandler)
|
||||
attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler)
|
||||
|
||||
attachHandler(http.MethodGet, InstanceRulesPath, m.InstanceRulesGETHandler)
|
||||
attachHandler(http.MethodGet, InstanceBlocklistPath, m.InstanceDomainBlocksGETHandler)
|
||||
attachHandler(http.MethodGet, InstanceAllowlistPath, m.InstanceDomainAllowsGETHandler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@
|
|||
package instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
|
|
@ -31,6 +33,8 @@ import (
|
|||
|
||||
// InstancePeersGETHandler swagger:operation GET /api/v1/instance/peers instancePeersGet
|
||||
//
|
||||
// List peer domains.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - instance
|
||||
|
|
@ -44,19 +48,32 @@ import (
|
|||
// type: string
|
||||
// description: |-
|
||||
// Comma-separated list of filters to apply to results. Recognized filters are:
|
||||
// - `open` -- include peers that are not suspended or silenced
|
||||
// - `suspended` -- include peers that have been suspended.
|
||||
// - `open` -- include known domains that are not in the domain blocklist
|
||||
// - `allowed` -- include domains that are in the domain allowlist
|
||||
// - `blocked` -- include domains that are in the domain blocklist
|
||||
// - `suspended` -- DEPRECATED! Use `blocked` instead. Same as `blocked`: include domains that are in the domain blocklist;
|
||||
//
|
||||
// If filter is `open`, only instances that haven't been suspended or silenced will be returned.
|
||||
// If filter is `open`, only domains that aren't in the blocklist will be shown.
|
||||
//
|
||||
// If filter is `suspended`, only suspended instances will be shown.
|
||||
// If filter is `blocked`, only domains that *are* in the blocklist will be shown.
|
||||
//
|
||||
// If filter is `open,suspended`, then all known instances will be returned.
|
||||
// If filter is `allowed`, only domains that are in the allowlist will be shown.
|
||||
//
|
||||
// If filter is `open,blocked`, then blocked domains and known domains not on the blocklist will be shown.
|
||||
//
|
||||
// If filter is `open,allowed`, then allowed domains and known domains not on the blocklist will be shown.
|
||||
//
|
||||
// If filter is an empty string or not set, then `open` will be assumed as the default.
|
||||
// in: query
|
||||
// required: false
|
||||
// default: "open"
|
||||
// default: flat
|
||||
// -
|
||||
// name: flat
|
||||
// type: boolean
|
||||
// description: If true, a "flat" array of strings will be returned corresponding to just domain names.
|
||||
// in: query
|
||||
// required: false
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer: []
|
||||
|
|
@ -67,12 +84,10 @@ import (
|
|||
// If no filter parameter is provided, or filter is empty, then a legacy,
|
||||
// Mastodon-API compatible response will be returned. This will consist of
|
||||
// just a 'flat' array of strings like `["example.com", "example.org"]`,
|
||||
// which corresponds to domains this instance peers with.
|
||||
//
|
||||
//
|
||||
// If a filter parameter is provided, then an array of objects with at least
|
||||
// a `domain` key set on each object will be returned.
|
||||
// which corresponds to setting a filter of `open` and flat=true.
|
||||
//
|
||||
// If a filter parameter is provided and flat is not true, then an array
|
||||
// of objects with at least a `domain` key set on each object will be returned.
|
||||
//
|
||||
// Domains that are silenced or suspended will also have a key
|
||||
// `suspended_at` or `silenced_at` that contains an iso8601 date string.
|
||||
|
|
@ -81,7 +96,6 @@ import (
|
|||
// will have some letters replaced by `*` to make it more difficult for
|
||||
// bad actors to target instances with harassment.
|
||||
//
|
||||
//
|
||||
// Whether a flat response or a more detailed response is returned, domains
|
||||
// will be sorted alphabetically by hostname.
|
||||
// schema:
|
||||
|
|
@ -116,45 +130,85 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var includeSuspended bool
|
||||
var includeOpen bool
|
||||
var flat bool
|
||||
var (
|
||||
includeBlocked bool
|
||||
includeAllowed bool
|
||||
includeOpen bool
|
||||
flatten bool
|
||||
)
|
||||
|
||||
if filterParam := c.Query(PeersFilterKey); filterParam != "" {
|
||||
filters := strings.Split(filterParam, ",")
|
||||
for _, f := range filters {
|
||||
trimmed := strings.TrimSpace(f)
|
||||
switch {
|
||||
case strings.EqualFold(trimmed, "suspended"):
|
||||
includeSuspended = true
|
||||
case strings.EqualFold(trimmed, "blocked") || strings.EqualFold(trimmed, "suspended"):
|
||||
includeBlocked = true
|
||||
case strings.EqualFold(trimmed, "allowed"):
|
||||
includeAllowed = true
|
||||
case strings.EqualFold(trimmed, "open"):
|
||||
includeOpen = true
|
||||
default:
|
||||
err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'suspended'", trimmed)
|
||||
err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'blocked', 'allowed', and 'suspended' (deprecated)", trimmed)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// default is to only include open domains, and present
|
||||
// Default is to only include open domains, and present
|
||||
// them in a 'flat' manner (just an array of strings),
|
||||
// to maintain compatibility with mastodon API
|
||||
// to maintain compatibility with the Mastodon API.
|
||||
includeOpen = true
|
||||
flat = true
|
||||
flatten = true
|
||||
}
|
||||
|
||||
if includeOpen && !config.GetInstanceExposePeers() && isUnauthenticated {
|
||||
err := fmt.Errorf("peers open query requires an authenticated account/user")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
if includeBlocked && isUnauthenticated && !config.GetInstanceExposeBlocklist() {
|
||||
const errText = "peers blocked query requires an authenticated account/user"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if includeSuspended && !config.GetInstanceExposeSuspended() && isUnauthenticated {
|
||||
err := fmt.Errorf("peers suspended query requires an authenticated account/user")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
if includeAllowed && isUnauthenticated && !config.GetInstanceExposeAllowlist() {
|
||||
const errText = "peers allowed query requires an authenticated account/user"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
data, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), includeSuspended, includeOpen, flat)
|
||||
if includeOpen && isUnauthenticated && !config.GetInstanceExposePeers() {
|
||||
const errText = "peers open query requires an authenticated account/user"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if includeBlocked && includeAllowed {
|
||||
const errText = "cannot include blocked + allowed filters at the same time"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if flatStr := c.Query(PeersFlatKey); flatStr != "" {
|
||||
var err error
|
||||
flatten, err = strconv.ParseBool(flatStr)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing 'flat' key as boolean: %w", err)
|
||||
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data, errWithCode := m.processor.InstancePeersGet(
|
||||
c.Request.Context(),
|
||||
includeBlocked,
|
||||
includeAllowed,
|
||||
includeOpen,
|
||||
flatten,
|
||||
false, // Don't include severity.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -136,13 +136,14 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspended() {
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts",
|
||||
"severity": "suspend"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedUnauthorized() {
|
||||
config.SetInstanceExposeSuspended(false)
|
||||
config.SetInstanceExposeBlocklist(false)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost())
|
||||
|
|
@ -159,11 +160,11 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedUnautho
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Unauthorized: peers suspended query requires an authenticated account/user"}`, string(b))
|
||||
suite.Equal(`{"error":"Unauthorized: peers blocked query requires an authenticated account/user"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthorized() {
|
||||
config.SetInstanceExposeSuspended(false)
|
||||
config.SetInstanceExposeBlocklist(false)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost())
|
||||
|
|
@ -186,7 +187,8 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthori
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts",
|
||||
"severity": "suspend"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
@ -219,11 +221,33 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAll() {
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts",
|
||||
"severity": "suspend"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllowed() {
|
||||
recorder := httptest.NewRecorder()
|
||||
baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost())
|
||||
requestURI := fmt.Sprintf("%s/%s?filter=allowed", baseURI, instance.InstancePeersPath)
|
||||
ctx := suite.newContext(recorder, http.MethodGet, requestURI, nil, "", false)
|
||||
|
||||
suite.instanceModule.InstancePeersGETHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
dst := new(bytes.Buffer)
|
||||
err = json.Indent(dst, b, "", " ")
|
||||
suite.NoError(err)
|
||||
suite.Equal(`[]`, dst.String())
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated() {
|
||||
err := suite.db.Put(context.Background(), >smodel.DomainBlock{
|
||||
ID: "01G633XTNK51GBADQZFZQDP6WR",
|
||||
|
|
@ -263,16 +287,55 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated()
|
|||
{
|
||||
"domain": "o*g.*u**.t**.*or*t.*r**ev**",
|
||||
"suspended_at": "2021-06-09T10:34:55.000Z",
|
||||
"comment": "just absolutely the worst, wowza"
|
||||
"comment": "just absolutely the worst, wowza",
|
||||
"severity": "suspend"
|
||||
},
|
||||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts",
|
||||
"severity": "suspend"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscatedFlat() {
|
||||
err := suite.db.Put(context.Background(), >smodel.DomainBlock{
|
||||
ID: "01G633XTNK51GBADQZFZQDP6WR",
|
||||
CreatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"),
|
||||
UpdatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"),
|
||||
Domain: "omg.just.the.worst.org.ever",
|
||||
CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
PublicComment: "just absolutely the worst, wowza",
|
||||
Obfuscate: util.Ptr(true),
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost())
|
||||
requestURI := fmt.Sprintf("%s/%s?filter=suspended,open&flat=true", baseURI, instance.InstancePeersPath)
|
||||
ctx := suite.newContext(recorder, http.MethodGet, requestURI, nil, "", false)
|
||||
|
||||
suite.instanceModule.InstancePeersGETHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
dst := new(bytes.Buffer)
|
||||
err = json.Indent(dst, b, "", " ")
|
||||
suite.NoError(err)
|
||||
suite.Equal(`[
|
||||
"example.org",
|
||||
"fossbros-anonymous.io",
|
||||
"o*g.*u**.t**.*or*t.*r**ev**",
|
||||
"replyguys.com"
|
||||
]`, dst.String())
|
||||
}
|
||||
|
||||
func (suite *InstancePeersGetTestSuite) TestInstancePeersGetFunkyParams() {
|
||||
recorder := httptest.NewRecorder()
|
||||
baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost())
|
||||
|
|
@ -289,7 +352,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetFunkyParams() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Bad Request: filter aaaaaaaaaaaaaaaaa not recognized; accepted values are 'open', 'suspended'"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: filter aaaaaaaaaaaaaaaaa not recognized; accepted values are 'open', 'blocked', 'allowed', and 'suspended' (deprecated)"}`, string(b))
|
||||
}
|
||||
|
||||
func TestInstancePeersGetTestSuite(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -32,14 +32,17 @@ type Domain struct {
|
|||
// Time at which this domain was silenced. Key will not be present on open domains.
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
SilencedAt string `json:"silenced_at,omitempty"`
|
||||
// If the domain is blocked, what's the publicly-stated reason for the block.
|
||||
// If the domain is blocked or allowed, what's the publicly-stated reason (if any).
|
||||
// Alternative to `public_comment` to be used when serializing/deserializing via /api/v1/instance.
|
||||
// example: they smell
|
||||
Comment *string `form:"comment" json:"comment,omitempty"`
|
||||
// If the domain is blocked, what's the publicly-stated reason for the block.
|
||||
// If the domain is blocked or allowed, what's the publicly-stated reason (if any).
|
||||
// Alternative to `comment` to be used when serializing/deserializing NOT via /api/v1/instance.
|
||||
// example: they smell
|
||||
PublicComment *string `form:"public_comment" json:"public_comment,omitempty"`
|
||||
// Severity of this entry.
|
||||
// Only ever set for domain blocks, and if set, always="suspend".
|
||||
Severity string `form:"severity" json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
// DomainPermission represents a permission applied to one domain (explicit block/allow).
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@ func fieldtag(field, tag string) string {
|
|||
//
|
||||
// Please note that if you update this struct's fields or tags, you
|
||||
// will need to regenerate the global Getter/Setter helpers by running:
|
||||
// `go run ./internal/config/gen/ -out ./internal/config/helpers.gen.go`
|
||||
// `go run ./internal/config/gen/ -out ./internal/config/helpers.gen.go`.
|
||||
//
|
||||
// You will need to have gofumpt installed in order for this to work:
|
||||
// https://github.com/mvdan/gofumpt.
|
||||
type Configuration struct {
|
||||
LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"`
|
||||
LogTimestampFormat string `name:"log-timestamp-format" usage:"Format to use for the log timestamp, as supported by Go's time.Layout"`
|
||||
|
|
@ -88,8 +91,10 @@ type Configuration struct {
|
|||
InstanceFederationMode string `name:"instance-federation-mode" usage:"Set instance federation mode."`
|
||||
InstanceFederationSpamFilter bool `name:"instance-federation-spam-filter" usage:"Enable basic spam filter heuristics for messages coming from other instances, and drop messages identified as spam"`
|
||||
InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"`
|
||||
InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"`
|
||||
InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"`
|
||||
InstanceExposeBlocklist bool `name:"instance-expose-blocklist" usage:"Expose list of blocked domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=blocked and /api/v1/instance/domain_blocks"`
|
||||
InstanceExposeBlocklistWeb bool `name:"instance-expose-blocklist-web" usage:"Expose list of explicitly blocked domains as webpage on /about/domain_blocks"`
|
||||
InstanceExposeAllowlist bool `name:"instance-expose-allowlist" usage:"Expose list of allowed domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=allowed and /api/v1/instance/domain_allows"`
|
||||
InstanceExposeAllowlistWeb bool `name:"instance-expose-allowlist-web" usage:"Expose list of explicitly allowed domains as webpage on /about/domain_allows"`
|
||||
InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"`
|
||||
InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."`
|
||||
InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"`
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ var Defaults = Configuration{
|
|||
InstanceFederationMode: InstanceFederationModeDefault,
|
||||
InstanceFederationSpamFilter: false,
|
||||
InstanceExposePeers: false,
|
||||
InstanceExposeSuspended: false,
|
||||
InstanceExposeSuspendedWeb: false,
|
||||
InstanceExposeBlocklist: false,
|
||||
InstanceExposeBlocklistWeb: false,
|
||||
InstanceDeliverToSharedInboxes: true,
|
||||
InstanceLanguages: make(language.Languages, 0),
|
||||
InstanceSubscriptionsProcessFrom: "23:00", // 11pm,
|
||||
|
|
|
|||
|
|
@ -63,8 +63,10 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
|
|||
flags.String("instance-federation-mode", cfg.InstanceFederationMode, "Set instance federation mode.")
|
||||
flags.Bool("instance-federation-spam-filter", cfg.InstanceFederationSpamFilter, "Enable basic spam filter heuristics for messages coming from other instances, and drop messages identified as spam")
|
||||
flags.Bool("instance-expose-peers", cfg.InstanceExposePeers, "Allow unauthenticated users to query /api/v1/instance/peers?filter=open")
|
||||
flags.Bool("instance-expose-suspended", cfg.InstanceExposeSuspended, "Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended")
|
||||
flags.Bool("instance-expose-suspended-web", cfg.InstanceExposeSuspendedWeb, "Expose list of suspended instances as webpage on /about/suspended")
|
||||
flags.Bool("instance-expose-blocklist", cfg.InstanceExposeBlocklist, "Expose list of blocked domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=blocked and /api/v1/instance/domain_blocks")
|
||||
flags.Bool("instance-expose-blocklist-web", cfg.InstanceExposeBlocklistWeb, "Expose list of explicitly blocked domains as webpage on /about/domain_blocks")
|
||||
flags.Bool("instance-expose-allowlist", cfg.InstanceExposeAllowlist, "Expose list of allowed domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=allowed and /api/v1/instance/domain_allows")
|
||||
flags.Bool("instance-expose-allowlist-web", cfg.InstanceExposeAllowlistWeb, "Expose list of explicitly allowed domains as webpage on /about/domain_allows")
|
||||
flags.Bool("instance-expose-public-timeline", cfg.InstanceExposePublicTimeline, "Allow unauthenticated users to query /api/v1/timelines/public")
|
||||
flags.Bool("instance-deliver-to-shared-inboxes", cfg.InstanceDeliverToSharedInboxes, "Deliver federated messages to shared inboxes, if they're available.")
|
||||
flags.Bool("instance-inject-mastodon-version", cfg.InstanceInjectMastodonVersion, "This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection")
|
||||
|
|
@ -79,7 +81,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
|
|||
flags.Int("accounts-registration-backlog-limit", cfg.AccountsRegistrationBacklogLimit, "Limit how big the 'accounts pending approval' queue can grow before registration is closed. 0 or less = no limit.")
|
||||
flags.Bool("accounts-allow-custom-css", cfg.AccountsAllowCustomCSS, "Allow accounts to enable custom CSS for their profile pages and statuses.")
|
||||
flags.Int("accounts-custom-css-length", cfg.AccountsCustomCSSLength, "Maximum permitted length (characters) of custom CSS for accounts.")
|
||||
flags.Int("accounts-max-profile-fields", cfg.AccountsMaxProfileFields, "Maximum amount of profile fields an account can have.")
|
||||
flags.Int("accounts-max-profile-fields", cfg.AccountsMaxProfileFields, "Maximum number of profile fields allowed for each account.")
|
||||
flags.Int("media-description-min-chars", cfg.MediaDescriptionMinChars, "Min required chars for an image description")
|
||||
flags.Int("media-description-max-chars", cfg.MediaDescriptionMaxChars, "Max permitted chars for an image description")
|
||||
flags.Int("media-remote-cache-days", cfg.MediaRemoteCacheDays, "Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely.")
|
||||
|
|
@ -207,7 +209,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
|
|||
}
|
||||
|
||||
func (cfg *Configuration) MarshalMap() map[string]any {
|
||||
cfgmap := make(map[string]any, 182)
|
||||
cfgmap := make(map[string]any, 184)
|
||||
cfgmap["log-level"] = cfg.LogLevel
|
||||
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
|
||||
cfgmap["log-db-queries"] = cfg.LogDbQueries
|
||||
|
|
@ -242,8 +244,10 @@ func (cfg *Configuration) MarshalMap() map[string]any {
|
|||
cfgmap["instance-federation-mode"] = cfg.InstanceFederationMode
|
||||
cfgmap["instance-federation-spam-filter"] = cfg.InstanceFederationSpamFilter
|
||||
cfgmap["instance-expose-peers"] = cfg.InstanceExposePeers
|
||||
cfgmap["instance-expose-suspended"] = cfg.InstanceExposeSuspended
|
||||
cfgmap["instance-expose-suspended-web"] = cfg.InstanceExposeSuspendedWeb
|
||||
cfgmap["instance-expose-blocklist"] = cfg.InstanceExposeBlocklist
|
||||
cfgmap["instance-expose-blocklist-web"] = cfg.InstanceExposeBlocklistWeb
|
||||
cfgmap["instance-expose-allowlist"] = cfg.InstanceExposeAllowlist
|
||||
cfgmap["instance-expose-allowlist-web"] = cfg.InstanceExposeAllowlistWeb
|
||||
cfgmap["instance-expose-public-timeline"] = cfg.InstanceExposePublicTimeline
|
||||
cfgmap["instance-deliver-to-shared-inboxes"] = cfg.InstanceDeliverToSharedInboxes
|
||||
cfgmap["instance-inject-mastodon-version"] = cfg.InstanceInjectMastodonVersion
|
||||
|
|
@ -674,19 +678,35 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
|
|||
}
|
||||
}
|
||||
|
||||
if ival, ok := cfgmap["instance-expose-suspended"]; ok {
|
||||
if ival, ok := cfgmap["instance-expose-blocklist"]; ok {
|
||||
var err error
|
||||
cfg.InstanceExposeSuspended, err = cast.ToBoolE(ival)
|
||||
cfg.InstanceExposeBlocklist, err = cast.ToBoolE(ival)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-suspended': %w", ival, err)
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-blocklist': %w", ival, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ival, ok := cfgmap["instance-expose-suspended-web"]; ok {
|
||||
if ival, ok := cfgmap["instance-expose-blocklist-web"]; ok {
|
||||
var err error
|
||||
cfg.InstanceExposeSuspendedWeb, err = cast.ToBoolE(ival)
|
||||
cfg.InstanceExposeBlocklistWeb, err = cast.ToBoolE(ival)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-suspended-web': %w", ival, err)
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-blocklist-web': %w", ival, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ival, ok := cfgmap["instance-expose-allowlist"]; ok {
|
||||
var err error
|
||||
cfg.InstanceExposeAllowlist, err = cast.ToBoolE(ival)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-allowlist': %w", ival, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ival, ok := cfgmap["instance-expose-allowlist-web"]; ok {
|
||||
var err error
|
||||
cfg.InstanceExposeAllowlistWeb, err = cast.ToBoolE(ival)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error casting %#v -> bool for 'instance-expose-allowlist-web': %w", ival, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2742,55 +2762,105 @@ func GetInstanceExposePeers() bool { return global.GetInstanceExposePeers() }
|
|||
// SetInstanceExposePeers safely sets the value for global configuration 'InstanceExposePeers' field
|
||||
func SetInstanceExposePeers(v bool) { global.SetInstanceExposePeers(v) }
|
||||
|
||||
// InstanceExposeSuspendedFlag returns the flag name for the 'InstanceExposeSuspended' field
|
||||
func InstanceExposeSuspendedFlag() string { return "instance-expose-suspended" }
|
||||
// InstanceExposeBlocklistFlag returns the flag name for the 'InstanceExposeBlocklist' field
|
||||
func InstanceExposeBlocklistFlag() string { return "instance-expose-blocklist" }
|
||||
|
||||
// GetInstanceExposeSuspended safely fetches the Configuration value for state's 'InstanceExposeSuspended' field
|
||||
func (st *ConfigState) GetInstanceExposeSuspended() (v bool) {
|
||||
// GetInstanceExposeBlocklist safely fetches the Configuration value for state's 'InstanceExposeBlocklist' field
|
||||
func (st *ConfigState) GetInstanceExposeBlocklist() (v bool) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.InstanceExposeSuspended
|
||||
v = st.config.InstanceExposeBlocklist
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetInstanceExposeSuspended safely sets the Configuration value for state's 'InstanceExposeSuspended' field
|
||||
func (st *ConfigState) SetInstanceExposeSuspended(v bool) {
|
||||
// SetInstanceExposeBlocklist safely sets the Configuration value for state's 'InstanceExposeBlocklist' field
|
||||
func (st *ConfigState) SetInstanceExposeBlocklist(v bool) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.InstanceExposeSuspended = v
|
||||
st.config.InstanceExposeBlocklist = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// GetInstanceExposeSuspended safely fetches the value for global configuration 'InstanceExposeSuspended' field
|
||||
func GetInstanceExposeSuspended() bool { return global.GetInstanceExposeSuspended() }
|
||||
// GetInstanceExposeBlocklist safely fetches the value for global configuration 'InstanceExposeBlocklist' field
|
||||
func GetInstanceExposeBlocklist() bool { return global.GetInstanceExposeBlocklist() }
|
||||
|
||||
// SetInstanceExposeSuspended safely sets the value for global configuration 'InstanceExposeSuspended' field
|
||||
func SetInstanceExposeSuspended(v bool) { global.SetInstanceExposeSuspended(v) }
|
||||
// SetInstanceExposeBlocklist safely sets the value for global configuration 'InstanceExposeBlocklist' field
|
||||
func SetInstanceExposeBlocklist(v bool) { global.SetInstanceExposeBlocklist(v) }
|
||||
|
||||
// InstanceExposeSuspendedWebFlag returns the flag name for the 'InstanceExposeSuspendedWeb' field
|
||||
func InstanceExposeSuspendedWebFlag() string { return "instance-expose-suspended-web" }
|
||||
// InstanceExposeBlocklistWebFlag returns the flag name for the 'InstanceExposeBlocklistWeb' field
|
||||
func InstanceExposeBlocklistWebFlag() string { return "instance-expose-blocklist-web" }
|
||||
|
||||
// GetInstanceExposeSuspendedWeb safely fetches the Configuration value for state's 'InstanceExposeSuspendedWeb' field
|
||||
func (st *ConfigState) GetInstanceExposeSuspendedWeb() (v bool) {
|
||||
// GetInstanceExposeBlocklistWeb safely fetches the Configuration value for state's 'InstanceExposeBlocklistWeb' field
|
||||
func (st *ConfigState) GetInstanceExposeBlocklistWeb() (v bool) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.InstanceExposeSuspendedWeb
|
||||
v = st.config.InstanceExposeBlocklistWeb
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetInstanceExposeSuspendedWeb safely sets the Configuration value for state's 'InstanceExposeSuspendedWeb' field
|
||||
func (st *ConfigState) SetInstanceExposeSuspendedWeb(v bool) {
|
||||
// SetInstanceExposeBlocklistWeb safely sets the Configuration value for state's 'InstanceExposeBlocklistWeb' field
|
||||
func (st *ConfigState) SetInstanceExposeBlocklistWeb(v bool) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.InstanceExposeSuspendedWeb = v
|
||||
st.config.InstanceExposeBlocklistWeb = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// GetInstanceExposeSuspendedWeb safely fetches the value for global configuration 'InstanceExposeSuspendedWeb' field
|
||||
func GetInstanceExposeSuspendedWeb() bool { return global.GetInstanceExposeSuspendedWeb() }
|
||||
// GetInstanceExposeBlocklistWeb safely fetches the value for global configuration 'InstanceExposeBlocklistWeb' field
|
||||
func GetInstanceExposeBlocklistWeb() bool { return global.GetInstanceExposeBlocklistWeb() }
|
||||
|
||||
// SetInstanceExposeSuspendedWeb safely sets the value for global configuration 'InstanceExposeSuspendedWeb' field
|
||||
func SetInstanceExposeSuspendedWeb(v bool) { global.SetInstanceExposeSuspendedWeb(v) }
|
||||
// SetInstanceExposeBlocklistWeb safely sets the value for global configuration 'InstanceExposeBlocklistWeb' field
|
||||
func SetInstanceExposeBlocklistWeb(v bool) { global.SetInstanceExposeBlocklistWeb(v) }
|
||||
|
||||
// InstanceExposeAllowlistFlag returns the flag name for the 'InstanceExposeAllowlist' field
|
||||
func InstanceExposeAllowlistFlag() string { return "instance-expose-allowlist" }
|
||||
|
||||
// GetInstanceExposeAllowlist safely fetches the Configuration value for state's 'InstanceExposeAllowlist' field
|
||||
func (st *ConfigState) GetInstanceExposeAllowlist() (v bool) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.InstanceExposeAllowlist
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetInstanceExposeAllowlist safely sets the Configuration value for state's 'InstanceExposeAllowlist' field
|
||||
func (st *ConfigState) SetInstanceExposeAllowlist(v bool) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.InstanceExposeAllowlist = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// GetInstanceExposeAllowlist safely fetches the value for global configuration 'InstanceExposeAllowlist' field
|
||||
func GetInstanceExposeAllowlist() bool { return global.GetInstanceExposeAllowlist() }
|
||||
|
||||
// SetInstanceExposeAllowlist safely sets the value for global configuration 'InstanceExposeAllowlist' field
|
||||
func SetInstanceExposeAllowlist(v bool) { global.SetInstanceExposeAllowlist(v) }
|
||||
|
||||
// InstanceExposeAllowlistWebFlag returns the flag name for the 'InstanceExposeAllowlistWeb' field
|
||||
func InstanceExposeAllowlistWebFlag() string { return "instance-expose-allowlist-web" }
|
||||
|
||||
// GetInstanceExposeAllowlistWeb safely fetches the Configuration value for state's 'InstanceExposeAllowlistWeb' field
|
||||
func (st *ConfigState) GetInstanceExposeAllowlistWeb() (v bool) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.InstanceExposeAllowlistWeb
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetInstanceExposeAllowlistWeb safely sets the Configuration value for state's 'InstanceExposeAllowlistWeb' field
|
||||
func (st *ConfigState) SetInstanceExposeAllowlistWeb(v bool) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.InstanceExposeAllowlistWeb = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// GetInstanceExposeAllowlistWeb safely fetches the value for global configuration 'InstanceExposeAllowlistWeb' field
|
||||
func GetInstanceExposeAllowlistWeb() bool { return global.GetInstanceExposeAllowlistWeb() }
|
||||
|
||||
// SetInstanceExposeAllowlistWeb safely sets the value for global configuration 'InstanceExposeAllowlistWeb' field
|
||||
func SetInstanceExposeAllowlistWeb(v bool) { global.SetInstanceExposeAllowlistWeb(v) }
|
||||
|
||||
// InstanceExposePublicTimelineFlag returns the flag name for the 'InstanceExposePublicTimeline' field
|
||||
func InstanceExposePublicTimelineFlag() string { return "instance-expose-public-timeline" }
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ package processing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
|
|
@ -31,6 +33,7 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/text"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util/xslices"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
|
|
@ -62,70 +65,126 @@ func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gt
|
|||
return ai, nil
|
||||
}
|
||||
|
||||
func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) {
|
||||
domains := []*apimodel.Domain{}
|
||||
func (p *Processor) InstancePeersGet(
|
||||
ctx context.Context,
|
||||
includeBlocked bool,
|
||||
includeAllowed bool,
|
||||
includeOpen bool,
|
||||
flatten bool,
|
||||
includeSeverity bool,
|
||||
) (any, gtserror.WithCode) {
|
||||
var (
|
||||
domainPerms []gtsmodel.DomainPermission
|
||||
apiDomains []*apimodel.Domain
|
||||
)
|
||||
|
||||
if includeBlocked {
|
||||
blocks, err := p.state.DB.GetDomainBlocks(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting domain blocks: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, block := range blocks {
|
||||
domainPerms = append(domainPerms, block)
|
||||
}
|
||||
|
||||
} else if includeAllowed {
|
||||
allows, err := p.state.DB.GetDomainAllows(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting domain allows: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, allow := range allows {
|
||||
domainPerms = append(domainPerms, allow)
|
||||
}
|
||||
}
|
||||
|
||||
for _, domainPerm := range domainPerms {
|
||||
// Domain may be in Punycode,
|
||||
// de-punify it just in case.
|
||||
domain := domainPerm.GetDomain()
|
||||
depunied, err := util.DePunify(domain)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if util.PtrOrZero(domainPerm.GetObfuscate()) {
|
||||
// Obfuscate the de-punified version.
|
||||
depunied = obfuscate(depunied)
|
||||
}
|
||||
|
||||
apiDomain := &apimodel.Domain{
|
||||
Domain: depunied,
|
||||
Comment: util.Ptr(domainPerm.GetPublicComment()),
|
||||
}
|
||||
|
||||
if domainPerm.GetType() == gtsmodel.DomainPermissionBlock {
|
||||
const severity = "suspend"
|
||||
apiDomain.Severity = severity
|
||||
suspendedAt := domainPerm.GetCreatedAt()
|
||||
apiDomain.SuspendedAt = util.FormatISO8601(suspendedAt)
|
||||
}
|
||||
|
||||
apiDomains = append(apiDomains, apiDomain)
|
||||
}
|
||||
|
||||
if includeOpen {
|
||||
instances, err := p.state.DB.GetInstancePeers(ctx, false)
|
||||
if err != nil && err != db.ErrNoEntries {
|
||||
err = fmt.Errorf("error selecting instance peers: %s", err)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err = gtserror.Newf("db error getting instance peers: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, i := range instances {
|
||||
for _, instance := range instances {
|
||||
// Domain may be in Punycode,
|
||||
// de-punify it just in case.
|
||||
d, err := util.DePunify(i.Domain)
|
||||
domain := instance.Domain
|
||||
depunied, err := util.DePunify(domain)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "couldn't depunify domain %s: %s", i.Domain, err)
|
||||
log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err)
|
||||
continue
|
||||
}
|
||||
|
||||
domains = append(domains, &apimodel.Domain{Domain: d})
|
||||
apiDomains = append(
|
||||
apiDomains,
|
||||
&apimodel.Domain{
|
||||
Domain: depunied,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if includeSuspended {
|
||||
domainBlocks := []*gtsmodel.DomainBlock{}
|
||||
if err := p.state.DB.GetAll(ctx, &domainBlocks); err != nil && err != db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
// Sort a-z.
|
||||
slices.SortFunc(
|
||||
apiDomains,
|
||||
func(a, b *apimodel.Domain) int {
|
||||
return strings.Compare(a.Domain, b.Domain)
|
||||
},
|
||||
)
|
||||
|
||||
for _, domainBlock := range domainBlocks {
|
||||
// Domain may be in Punycode,
|
||||
// de-punify it just in case.
|
||||
d, err := util.DePunify(domainBlock.Domain)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "couldn't depunify domain %s: %s", domainBlock.Domain, err)
|
||||
continue
|
||||
}
|
||||
// Deduplicate.
|
||||
apiDomains = xslices.DeduplicateFunc(
|
||||
apiDomains,
|
||||
func(v *apimodel.Domain) string {
|
||||
return v.Domain
|
||||
},
|
||||
)
|
||||
|
||||
if *domainBlock.Obfuscate {
|
||||
// Obfuscate the de-punified version.
|
||||
d = obfuscate(d)
|
||||
}
|
||||
|
||||
domains = append(domains, &apimodel.Domain{
|
||||
Domain: d,
|
||||
SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt),
|
||||
Comment: &domainBlock.PublicComment,
|
||||
})
|
||||
}
|
||||
if flatten {
|
||||
// Return just the domains.
|
||||
return xslices.Gather(
|
||||
[]string{},
|
||||
apiDomains,
|
||||
func(v *apimodel.Domain) string {
|
||||
return v.Domain
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
sort.Slice(domains, func(i, j int) bool {
|
||||
return domains[i].Domain < domains[j].Domain
|
||||
})
|
||||
|
||||
if flat {
|
||||
flattened := []string{}
|
||||
for _, d := range domains {
|
||||
flattened = append(flattened, d.Domain)
|
||||
}
|
||||
return flattened, nil
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
return apiDomains, nil
|
||||
}
|
||||
|
||||
func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRule, gtserror.WithCode) {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ func (m *Module) aboutGETHandler(c *gin.Context) {
|
|||
Stylesheets: []string{cssAbout},
|
||||
Extra: map[string]any{
|
||||
"showStrap": true,
|
||||
"blocklistExposed": config.GetInstanceExposeSuspendedWeb(),
|
||||
"blocklistExposed": config.GetInstanceExposeBlocklistWeb(),
|
||||
"allowlistExposed": config.GetInstanceExposeAllowlistWeb(),
|
||||
"languages": config.GetInstanceLanguages().DisplayStrs(),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
// 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 web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
domainBlockListPath = aboutPath + "/suspended"
|
||||
)
|
||||
|
||||
func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.GetInstanceExposeSuspendedWeb() {
|
||||
err := fmt.Errorf("this instance does not publicy expose its blocklist")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "domain-blocklist.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA},
|
||||
Extra: map[string]any{"blocklist": domainBlocks},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
136
internal/web/domainperms.go
Normal file
136
internal/web/domainperms.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// 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 web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
domainBlocklistPath = aboutPath + "/domain_blocks"
|
||||
domainAllowlistPath = aboutPath + "/domain_allows"
|
||||
)
|
||||
|
||||
func (m *Module) domainBlocklistGETHandler(c *gin.Context) {
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.GetInstanceExposeBlocklistWeb() {
|
||||
const errText = "this instance does not expose its blocklist via the web"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlocks, errWithCode := m.processor.InstancePeersGet(
|
||||
c.Request.Context(),
|
||||
true, // Include blocked.
|
||||
false, // Don't include allowed.
|
||||
false, // Don't include open.
|
||||
false, // Don't flatten list.
|
||||
false, // Don't include severity.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "domain-blocklist.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA},
|
||||
Extra: map[string]any{"blocklist": domainBlocks},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
func (m *Module) domainAllowlistGETHandler(c *gin.Context) {
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.GetInstanceExposeAllowlistWeb() {
|
||||
const errText = "this instance does not expose its allowlist via the web"
|
||||
errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainAllows, errWithCode := m.processor.InstancePeersGet(
|
||||
c.Request.Context(),
|
||||
false, // Don't include blocked.
|
||||
true, // Include allowed.
|
||||
false, // Don't include open.
|
||||
false, // Don't flatten list.
|
||||
false, // Don't include severity.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "domain-allowlist.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA},
|
||||
Extra: map[string]any{"allowlist": domainAllows},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
@ -127,7 +127,8 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
|||
everythingElseGroup.Handle(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, aboutPath, m.aboutGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, loginPath, m.loginGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, domainBlocklistPath, m.domainBlocklistGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, domainAllowlistPath, m.domainAllowlistGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, tagsPath, m.tagGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodGet, signupPath, m.signupGETHandler)
|
||||
everythingElseGroup.Handle(http.MethodPost, signupPath, m.signupPOSTHandler)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue