mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-18 09:27:34 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
commit
0e137c0f2d
1759 changed files with 864109 additions and 314186 deletions
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -73,6 +74,7 @@ func (suite *EmojiGetTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
suite.tc = typeutils.NewConverter(&suite.state)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() {
|
|||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"first": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"totalItems": 8,
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollection"
|
||||
}`, dst.String())
|
||||
|
||||
|
|
@ -142,6 +142,14 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
|
|||
"id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40",
|
||||
"next": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"orderedItems": [
|
||||
{
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR/activity#Create",
|
||||
"object": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Create"
|
||||
},
|
||||
{
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
|
|
@ -160,8 +168,8 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
|
|||
}
|
||||
],
|
||||
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01HH9KYNQPA416TNJ53NSATP40",
|
||||
"totalItems": 8,
|
||||
"prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollectionPage"
|
||||
}`, dst.String())
|
||||
|
||||
|
|
@ -224,7 +232,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
|
|||
"id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40&max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"orderedItems": [],
|
||||
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"totalItems": 8,
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollectionPage"
|
||||
}`, dst.String())
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package users_test
|
|||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -84,6 +85,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.tc = typeutils.NewConverter(&suite.state)
|
||||
|
||||
testrig.StartTimelines(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/gin-contrib/sessions/memstore"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/auth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -84,6 +85,7 @@ func (suite *AuthStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/announcements"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/apps"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks"
|
||||
|
|
@ -66,6 +67,7 @@ type Client struct {
|
|||
|
||||
accounts *accounts.Module // api/v1/accounts, api/v1/profile
|
||||
admin *admin.Module // api/v1/admin
|
||||
announcements *announcements.Module // api/v1/announcements
|
||||
apps *apps.Module // api/v1/apps
|
||||
blocks *blocks.Module // api/v1/blocks
|
||||
bookmarks *bookmarks.Module // api/v1/bookmarks
|
||||
|
|
@ -117,6 +119,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) {
|
|||
h := apiGroup.Handle
|
||||
c.accounts.Route(h)
|
||||
c.admin.Route(h)
|
||||
c.announcements.Route(h)
|
||||
c.apps.Route(h)
|
||||
c.blocks.Route(h)
|
||||
c.bookmarks.Route(h)
|
||||
|
|
@ -156,6 +159,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client {
|
|||
|
||||
accounts: accounts.New(p),
|
||||
admin: admin.New(state, p),
|
||||
announcements: announcements.New(p),
|
||||
apps: apps.New(p),
|
||||
blocks: blocks.New(p),
|
||||
bookmarks: bookmarks.New(p),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -85,6 +86,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const (
|
|||
|
||||
BlockPath = BasePathWithID + "/block"
|
||||
DeletePath = BasePath + "/delete"
|
||||
FeaturedTagsPath = BasePathWithID + "/featured_tags"
|
||||
FollowersPath = BasePathWithID + "/followers"
|
||||
FollowingPath = BasePathWithID + "/following"
|
||||
FollowPath = BasePathWithID + "/follow"
|
||||
|
|
@ -98,6 +99,9 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
|||
// get account's statuses
|
||||
attachHandler(http.MethodGet, StatusesPath, m.AccountStatusesGETHandler)
|
||||
|
||||
// get account's featured tags
|
||||
attachHandler(http.MethodGet, FeaturedTagsPath, m.AccountFeaturedTagsGETHandler)
|
||||
|
||||
// get following or followers
|
||||
attachHandler(http.MethodGet, FollowersPath, m.AccountFollowersGETHandler)
|
||||
attachHandler(http.MethodGet, FollowingPath, m.AccountFollowingGETHandler)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
|
@ -98,8 +97,8 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
|
|||
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", apimodelAccount.HeaderStatic)
|
||||
suite.Equal(2, apimodelAccount.FollowersCount)
|
||||
suite.Equal(2, apimodelAccount.FollowingCount)
|
||||
suite.Equal(8, apimodelAccount.StatusesCount)
|
||||
suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy)
|
||||
suite.Equal(9, apimodelAccount.StatusesCount)
|
||||
suite.EqualValues(apimodel.VisibilityPublic, apimodelAccount.Source.Privacy)
|
||||
suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language)
|
||||
suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)
|
||||
}
|
||||
|
|
|
|||
83
internal/api/client/accounts/featuredtags.go
Normal file
83
internal/api/client/accounts/featuredtags.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// 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 accounts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountFeaturedTagsGETHandler swagger:operation GET /api/v1/accounts/{id}/featured_tags accountsFeaturedTags
|
||||
//
|
||||
// Get an array of target account's featured tags.
|
||||
//
|
||||
// THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - accounts
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the account.
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:accounts
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// type: object
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountFeaturedTagsGETHandler(c *gin.Context) {
|
||||
_, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), 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
|
||||
}
|
||||
|
||||
apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
|
||||
}
|
||||
|
|
@ -19,9 +19,7 @@ package accounts
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -140,25 +138,15 @@ func normalizeCreateUpdateMute(form *apimodel.UserMuteCreateUpdateRequest) error
|
|||
// Apply defaults for missing fields.
|
||||
form.Notifications = util.Ptr(util.PtrOrValue(form.Notifications, false))
|
||||
|
||||
// Normalize mute duration if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.DurationI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.Duration = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
duration, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse duration value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.Duration = &duration
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// Normalize duration if necessary.
|
||||
if form.DurationI != nil {
|
||||
// If we parsed this as JSON, duration
|
||||
// may be either a float64 or a string.
|
||||
duration, err := apiutil.ParseDuration(form.DurationI, "duration")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Duration = duration
|
||||
}
|
||||
|
||||
// Interpret zero as indefinite duration.
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
|
|||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||
}
|
||||
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01J5QVB9VC76NPPRQ207GG4DRZ&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
}
|
||||
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
|
||||
|
|
|
|||
|
|
@ -96,10 +96,11 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -154,10 +155,11 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -208,6 +210,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
|
@ -252,13 +255,15 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
|
||||
"avatar_description": "a green goblin looking nasty",
|
||||
"avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
|
||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
|
||||
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10T09:24:00.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
@ -302,6 +307,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
|
@ -348,10 +354,11 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2023-11-02T10:44:25.000Z",
|
||||
"last_status_at": "2023-11-02",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -393,10 +400,11 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -439,6 +447,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.webp",
|
||||
"header_description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3R",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
|
@ -484,6 +493,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
|
@ -568,6 +578,7 @@ func (suite *AccountsGetTestSuite) TestAccountsMinID() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
|
|
|||
|
|
@ -28,37 +28,48 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
BasePath = "/v1/admin"
|
||||
EmojiPath = BasePath + "/custom_emojis"
|
||||
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
|
||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
|
||||
DomainAllowsPath = BasePath + "/domain_allows"
|
||||
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
|
||||
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
|
||||
HeaderAllowsPath = BasePath + "/header_allows"
|
||||
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
|
||||
HeaderBlocksPath = BasePath + "/header_blocks"
|
||||
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
|
||||
AccountsV1Path = BasePath + "/accounts"
|
||||
AccountsV2Path = "/v2/admin/accounts"
|
||||
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
|
||||
AccountsActionPath = AccountsPathWithID + "/action"
|
||||
AccountsApprovePath = AccountsPathWithID + "/approve"
|
||||
AccountsRejectPath = AccountsPathWithID + "/reject"
|
||||
MediaCleanupPath = BasePath + "/media_cleanup"
|
||||
MediaRefetchPath = BasePath + "/media_refetch"
|
||||
ReportsPath = BasePath + "/reports"
|
||||
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
|
||||
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
||||
EmailPath = BasePath + "/email"
|
||||
EmailTestPath = EmailPath + "/test"
|
||||
InstanceRulesPath = BasePath + "/instance/rules"
|
||||
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
|
||||
DebugPath = BasePath + "/debug"
|
||||
DebugAPUrlPath = DebugPath + "/apurl"
|
||||
DebugClearCachesPath = DebugPath + "/caches/clear"
|
||||
BasePath = "/v1/admin"
|
||||
EmojiPath = BasePath + "/custom_emojis"
|
||||
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
|
||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
|
||||
DomainAllowsPath = BasePath + "/domain_allows"
|
||||
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
|
||||
DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
|
||||
DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
|
||||
DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
|
||||
DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
|
||||
DomainPermissionExcludesPath = BasePath + "/domain_permission_excludes"
|
||||
DomainPermissionExcludesPathWithID = DomainPermissionExcludesPath + "/:" + apiutil.IDKey
|
||||
DomainPermissionSubscriptionsPath = BasePath + "/domain_permission_subscriptions"
|
||||
DomainPermissionSubscriptionsPathWithID = DomainPermissionSubscriptionsPath + "/:" + apiutil.IDKey
|
||||
DomainPermissionSubscriptionsPreviewPath = DomainPermissionSubscriptionsPath + "/preview"
|
||||
DomainPermissionSubscriptionRemovePath = DomainPermissionSubscriptionsPathWithID + "/remove"
|
||||
DomainPermissionSubscriptionTestPath = DomainPermissionSubscriptionsPathWithID + "/test"
|
||||
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
|
||||
HeaderAllowsPath = BasePath + "/header_allows"
|
||||
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
|
||||
HeaderBlocksPath = BasePath + "/header_blocks"
|
||||
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
|
||||
AccountsV1Path = BasePath + "/accounts"
|
||||
AccountsV2Path = "/v2/admin/accounts"
|
||||
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
|
||||
AccountsActionPath = AccountsPathWithID + "/action"
|
||||
AccountsApprovePath = AccountsPathWithID + "/approve"
|
||||
AccountsRejectPath = AccountsPathWithID + "/reject"
|
||||
MediaCleanupPath = BasePath + "/media_cleanup"
|
||||
MediaRefetchPath = BasePath + "/media_refetch"
|
||||
ReportsPath = BasePath + "/reports"
|
||||
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
|
||||
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
||||
EmailPath = BasePath + "/email"
|
||||
EmailTestPath = EmailPath + "/test"
|
||||
InstanceRulesPath = BasePath + "/instance/rules"
|
||||
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
|
||||
DebugPath = BasePath + "/debug"
|
||||
DebugAPUrlPath = DebugPath + "/apurl"
|
||||
DebugClearCachesPath = DebugPath + "/caches/clear"
|
||||
|
||||
FilterQueryKey = "filter"
|
||||
MaxShortcodeDomainKey = "max_shortcode_domain"
|
||||
|
|
@ -99,6 +110,28 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
|||
attachHandler(http.MethodGet, DomainAllowsPathWithID, m.DomainAllowGETHandler)
|
||||
attachHandler(http.MethodDelete, DomainAllowsPathWithID, m.DomainAllowDELETEHandler)
|
||||
|
||||
// domain permission draft stuff
|
||||
attachHandler(http.MethodPost, DomainPermissionDraftsPath, m.DomainPermissionDraftsPOSTHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionDraftsPath, m.DomainPermissionDraftsGETHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionDraftsPathWithID, m.DomainPermissionDraftGETHandler)
|
||||
attachHandler(http.MethodPost, DomainPermissionDraftAcceptPath, m.DomainPermissionDraftAcceptPOSTHandler)
|
||||
attachHandler(http.MethodPost, DomainPermissionDraftRemovePath, m.DomainPermissionDraftRemovePOSTHandler)
|
||||
|
||||
// domain permission excludes stuff
|
||||
attachHandler(http.MethodPost, DomainPermissionExcludesPath, m.DomainPermissionExcludesPOSTHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionExcludesPath, m.DomainPermissionExcludesGETHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeGETHandler)
|
||||
attachHandler(http.MethodDelete, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeDELETEHandler)
|
||||
|
||||
// domain permission subscriptions stuff
|
||||
attachHandler(http.MethodPost, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionPOSTHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionsGETHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPreviewPath, m.DomainPermissionSubscriptionsPreviewGETHandler)
|
||||
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionGETHandler)
|
||||
attachHandler(http.MethodPatch, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionPATCHHandler)
|
||||
attachHandler(http.MethodPost, DomainPermissionSubscriptionRemovePath, m.DomainPermissionSubscriptionRemovePOSTHandler)
|
||||
attachHandler(http.MethodPost, DomainPermissionSubscriptionTestPath, m.DomainPermissionSubscriptionTestPOSTHandler)
|
||||
|
||||
// header filtering administration routes
|
||||
attachHandler(http.MethodGet, HeaderAllowsPathWithID, m.HeaderFilterAllowGET)
|
||||
attachHandler(http.MethodGet, HeaderBlocksPathWithID, m.HeaderFilterBlockGET)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
adminactions "github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -91,6 +92,7 @@ func (suite *AdminStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = adminactions.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -302,3 +302,45 @@ func (m *Module) getDomainPermissions(
|
|||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
|
||||
// parseDomainPermissionType is a util function to parse i
|
||||
// to a DomainPermissionType, or return a suitable error.
|
||||
func parseDomainPermissionType(i string) (
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
errWithCode gtserror.WithCode,
|
||||
) {
|
||||
if i == "" {
|
||||
const errText = "permission_type not set, must be one of block or allow"
|
||||
errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
return
|
||||
}
|
||||
|
||||
permType = gtsmodel.ParseDomainPermissionType(i)
|
||||
if permType == gtsmodel.DomainPermissionUnknown {
|
||||
var errText = fmt.Sprintf("permission_type %s not recognized, must be one of block or allow", i)
|
||||
errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseDomainPermSubContentType is a util function to parse i
|
||||
// to a DomainPermSubContentType, or return a suitable error.
|
||||
func parseDomainPermSubContentType(i string) (
|
||||
contentType gtsmodel.DomainPermSubContentType,
|
||||
errWithCode gtserror.WithCode,
|
||||
) {
|
||||
if i == "" {
|
||||
const errText = "content_type not set, must be one of text/csv, text/plain or application/json"
|
||||
errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
return
|
||||
}
|
||||
|
||||
contentType = gtsmodel.NewDomainPermSubContentType(i)
|
||||
if contentType == gtsmodel.DomainPermSubContentTypeUnknown {
|
||||
var errText = fmt.Sprintf("content_type %s not recognized, must be one of text/csv, text/plain or application/json", i)
|
||||
errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
134
internal/api/client/admin/domainpermissiondraftaccept.go
Normal file
134
internal/api/client/admin/domainpermissiondraftaccept.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftAcceptPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/accept domainPermissionDraftAccept
|
||||
//
|
||||
// Accept a domain permission draft, turning it into an enforced domain permission.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
// -
|
||||
// name: overwrite
|
||||
// in: formData
|
||||
// description: >-
|
||||
// If a domain permission already exists with the same
|
||||
// domain and permission type as the draft, overwrite
|
||||
// the existing permission with fields from the draft.
|
||||
// type: boolean
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The newly created domain permission.
|
||||
// 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
|
||||
func (m *Module) DomainPermissionDraftAcceptPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
type AcceptForm struct {
|
||||
Overwrite bool `json:"overwrite" form:"overwrite"`
|
||||
}
|
||||
|
||||
form := new(AcceptForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, _, errWithCode := m.processor.Admin().DomainPermissionDraftAccept(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
form.Overwrite,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
159
internal/api/client/admin/domainpermissiondraftcreate.go
Normal file
159
internal/api/client/admin/domainpermissiondraftcreate.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftsPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts domainPermissionDraftCreate
|
||||
//
|
||||
// Create a domain permission draft with the given parameters.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: domain
|
||||
// in: formData
|
||||
// description: Domain to create the permission draft for.
|
||||
// type: string
|
||||
// -
|
||||
// name: permission_type
|
||||
// in: formData
|
||||
// description: Create a draft "allow" or a draft "block".
|
||||
// type: string
|
||||
// -
|
||||
// name: obfuscate
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Obfuscate the name of the domain when serving it publicly.
|
||||
// Eg., `example.org` becomes something like `ex***e.org`.
|
||||
// type: boolean
|
||||
// -
|
||||
// name: public_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Public comment about this domain permission.
|
||||
// This will be displayed alongside the domain permission if you choose to share permissions.
|
||||
// type: string
|
||||
// -
|
||||
// name: private_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Private comment about this domain permission. Will only be shown to other admins, so this
|
||||
// is a useful way of internally keeping track of why a certain domain ended up permissioned.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The newly created domain permission draft.
|
||||
// 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
|
||||
func (m *Module) DomainPermissionDraftsPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse + validate form.
|
||||
form := new(apimodel.DomainPermissionRequest)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Domain == "" {
|
||||
const errText = "domain must be set"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permType, errWithCode := parseDomainPermissionType(form.PermissionType)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permDraft, errWithCode := m.processor.Admin().DomainPermissionDraftCreate(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
form.Domain,
|
||||
permType,
|
||||
form.Obfuscate,
|
||||
form.PublicComment,
|
||||
form.PrivateComment,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permDraft)
|
||||
}
|
||||
104
internal/api/client/admin/domainpermissiondraftget.go
Normal file
104
internal/api/client/admin/domainpermissiondraftget.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts/{id} domainPermissionDraftGet
|
||||
//
|
||||
// Get domain permission draft with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission draft.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionDraftGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permDraft, errWithCode := m.processor.Admin().DomainPermissionDraftGet(c.Request.Context(), id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permDraft)
|
||||
}
|
||||
134
internal/api/client/admin/domainpermissiondraftremove.go
Normal file
134
internal/api/client/admin/domainpermissiondraftremove.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/remove domainPermissionDraftRemove
|
||||
//
|
||||
// Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
// -
|
||||
// name: exclude_target
|
||||
// in: formData
|
||||
// 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.
|
||||
// type: boolean
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The removed domain permission draft.
|
||||
// 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
|
||||
func (m *Module) DomainPermissionDraftRemovePOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
type RemoveForm struct {
|
||||
ExcludeTarget bool `json:"exclude_target" form:"exclude_target"`
|
||||
}
|
||||
|
||||
form := new(RemoveForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, errWithCode := m.processor.Admin().DomainPermissionDraftRemove(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
form.ExcludeTarget,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
185
internal/api/client/admin/domainpermissiondraftsget.go
Normal file
185
internal/api/client/admin/domainpermissiondraftsget.go
Normal 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 admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftsGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts domainPermissionDraftsGet
|
||||
//
|
||||
// View domain permission drafts.
|
||||
//
|
||||
// The drafts 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_drafts?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: subscription_id
|
||||
// type: string
|
||||
// description: Show only drafts created by the given subscription ID.
|
||||
// in: query
|
||||
// -
|
||||
// name: domain
|
||||
// type: string
|
||||
// description: Return only drafts that target the given domain.
|
||||
// in: query
|
||||
// -
|
||||
// name: permission_type
|
||||
// type: string
|
||||
// description: Filter on "block" or "allow" type drafts.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// 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: since_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: min_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: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// minimum: 1
|
||||
// maximum: 100
|
||||
// in: query
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission drafts.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '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) DomainPermissionDraftsGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permTypeStr := c.Query(apiutil.DomainPermissionPermTypeKey)
|
||||
permType := gtsmodel.ParseDomainPermissionType(permTypeStr)
|
||||
if permTypeStr != "" && permType == gtsmodel.DomainPermissionUnknown {
|
||||
text := fmt.Sprintf(
|
||||
"permission_type %s not recognized, valid values are empty string, block, or allow",
|
||||
permTypeStr,
|
||||
)
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionDraftsGet(
|
||||
c.Request.Context(),
|
||||
c.Query(apiutil.DomainPermissionSubscriptionIDKey),
|
||||
c.Query(apiutil.DomainPermissionDomainKey),
|
||||
permType,
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
138
internal/api/client/admin/domainpermissionexcludecreate.go
Normal file
138
internal/api/client/admin/domainpermissionexcludecreate.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludesPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_excludes domainPermissionExcludeCreate
|
||||
//
|
||||
// Create a domain permission exclude with the given parameters.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: domain
|
||||
// in: formData
|
||||
// description: Domain to create the permission exclude for.
|
||||
// type: string
|
||||
// -
|
||||
// name: private_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Private comment about this domain exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// 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
|
||||
func (m *Module) DomainPermissionExcludesPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse + validate form.
|
||||
type ExcludeForm struct {
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
PrivateComment string `form:"private_comment" json:"private_comment"`
|
||||
}
|
||||
|
||||
form := new(ExcludeForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Domain == "" {
|
||||
const errText = "domain must be set"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permExclude, errWithCode := m.processor.Admin().DomainPermissionExcludeCreate(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
form.Domain,
|
||||
form.PrivateComment,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permExclude)
|
||||
}
|
||||
104
internal/api/client/admin/domainpermissionexcludeget.go
Normal file
104
internal/api/client/admin/domainpermissionexcludeget.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludeGETHandler swagger:operation GET /api/v1/admin/domain_permission_excludes/{id} domainPermissionExcludeGet
|
||||
//
|
||||
// Get domain permission exclude with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// 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
|
||||
func (m *Module) DomainPermissionExcludeGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permExclude, errWithCode := m.processor.Admin().DomainPermissionExcludeGet(c.Request.Context(), id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permExclude)
|
||||
}
|
||||
110
internal/api/client/admin/domainpermissionexcluderemove.go
Normal file
110
internal/api/client/admin/domainpermissionexcluderemove.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludeDELETEHandler swagger:operation DELETE /api/v1/admin/domain_permission_excludes/{id} domainPermissionExcludeDelete
|
||||
//
|
||||
// Remove a domain permission exclude.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// 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
|
||||
func (m *Module) DomainPermissionExcludeDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, errWithCode := m.processor.Admin().DomainPermissionExcludeRemove(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
159
internal/api/client/admin/domainpermissionexcludesget.go
Normal file
159
internal/api/client/admin/domainpermissionexcludesget.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludesGETHandler swagger:operation GET /api/v1/admin/domain_permission_excludes domainPermissionExcludesGet
|
||||
//
|
||||
// View domain permission excludes.
|
||||
//
|
||||
// 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"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: domain
|
||||
// type: string
|
||||
// description: Return only excludes that target the given domain.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// 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: since_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: min_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: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// minimum: 1
|
||||
// maximum: 100
|
||||
// in: query
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission excludes.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '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) DomainPermissionExcludesGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionExcludesGet(
|
||||
c.Request.Context(),
|
||||
c.Query(apiutil.DomainPermissionDomainKey),
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
244
internal/api/client/admin/domainpermissionsubscriptioncreate.go
Normal file
244
internal/api/client/admin/domainpermissionsubscriptioncreate.go
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionCreate
|
||||
//
|
||||
// Create a domain permission subscription with the given parameters.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: priority
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Priority of this subscription compared to others of the same permission type.
|
||||
// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
|
||||
// permissions generated by lower priority subscriptions. When two subscriptions
|
||||
// have the same `priority` value, priority is indeterminate, so it's recommended
|
||||
// to always set this value manually.
|
||||
// type: number
|
||||
// minimum: 0
|
||||
// maximum: 255
|
||||
// default: 0
|
||||
// -
|
||||
// name: title
|
||||
// in: formData
|
||||
// description: Optional title for this subscription.
|
||||
// type: string
|
||||
// -
|
||||
// name: permission_type
|
||||
// required: true
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Type of permissions to create by parsing the targeted file/list.
|
||||
// One of "allow" or "block".
|
||||
// type: string
|
||||
// -
|
||||
// name: as_draft
|
||||
// in: formData
|
||||
// description: >-
|
||||
// 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.
|
||||
// Defaults to "true".
|
||||
// type: boolean
|
||||
// default: true
|
||||
// -
|
||||
// name: adopt_orphans
|
||||
// in: formData
|
||||
// description: >-
|
||||
// If true, this domain permission subscription will "adopt" domain permissions
|
||||
// which already exist on the instance, and which meet the following conditions:
|
||||
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
|
||||
// in the subscribed list. Such orphaned domain permissions will be given this
|
||||
// subscription's subscription ID value and be managed by this subscription.
|
||||
// type: boolean
|
||||
// default: false
|
||||
// -
|
||||
// name: uri
|
||||
// required: true
|
||||
// in: formData
|
||||
// description: URI to call in order to fetch the permissions list.
|
||||
// type: string
|
||||
// -
|
||||
// name: content_type
|
||||
// required: true
|
||||
// in: formData
|
||||
// description: >-
|
||||
// MIME content type to use when parsing the permissions list.
|
||||
// One of "text/plain", "text/csv", and "application/json".
|
||||
// type: string
|
||||
// -
|
||||
// name: fetch_username
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Optional basic auth username to provide when fetching given uri.
|
||||
// If set, will be transmitted along with `fetch_password` when doing the fetch.
|
||||
// type: string
|
||||
// -
|
||||
// name: fetch_password
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Optional basic auth password to provide when fetching given uri.
|
||||
// If set, will be transmitted along with `fetch_username` when doing the fetch.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The newly created domain permission subscription.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionSubscriptionPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse + validate form.
|
||||
form := new(apimodel.DomainPermissionSubscriptionRequest)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Check priority.
|
||||
// Default to 0.
|
||||
priority := util.PtrOrZero(form.Priority)
|
||||
if priority < 0 || priority > 255 {
|
||||
const errText = "priority must be a number in the range 0 to 255"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure URI is set.
|
||||
if form.URI == nil {
|
||||
const errText = "uri must be set"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure URI is parseable.
|
||||
uri, err := url.Parse(*form.URI)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid uri provided: %w", err)
|
||||
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize URI by converting back to string.
|
||||
uriStr := uri.String()
|
||||
|
||||
// Content type must be set.
|
||||
contentTypeStr := util.PtrOrZero(form.ContentType)
|
||||
contentType, errWithCode := parseDomainPermSubContentType(contentTypeStr)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Permission type must be set.
|
||||
permTypeStr := util.PtrOrZero(form.PermissionType)
|
||||
permType, errWithCode := parseDomainPermissionType(permTypeStr)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Default `as_draft` to true.
|
||||
asDraft := util.PtrOrValue(form.AsDraft, true)
|
||||
|
||||
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionCreate(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
uint8(priority), // #nosec G115 -- Validated above.
|
||||
util.PtrOrZero(form.Title), // Optional.
|
||||
uriStr,
|
||||
contentType,
|
||||
permType,
|
||||
asDraft,
|
||||
util.PtrOrZero(form.FetchUsername), // Optional.
|
||||
util.PtrOrZero(form.FetchPassword), // Optional.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permSub)
|
||||
}
|
||||
104
internal/api/client/admin/domainpermissionsubscriptionget.go
Normal file
104
internal/api/client/admin/domainpermissionsubscriptionget.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions/{id} domainPermissionSubscriptionGet
|
||||
//
|
||||
// Get domain permission subscription with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission subscription.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission subscription.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionSubscriptionGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionGet(c.Request.Context(), id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permSub)
|
||||
}
|
||||
143
internal/api/client/admin/domainpermissionsubscriptionremove.go
Normal file
143
internal/api/client/admin/domainpermissionsubscriptionremove.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions/{id}/remove domainPermissionSubscriptionRemove
|
||||
//
|
||||
// Remove a domain permission subscription.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission subscription.
|
||||
// type: string
|
||||
// -
|
||||
// name: remove_children
|
||||
// in: formData
|
||||
// description: >-
|
||||
// When removing the domain permission subscription, also
|
||||
// remove children of this subscription, ie., domain permissions
|
||||
// that are managed by this subscription. If false, then children
|
||||
// will instead be orphaned but not removed.
|
||||
//
|
||||
// Note that removed permissions may end up being created again later
|
||||
// by another domain permission subscription of lower priority than
|
||||
// the removed subscription. Likewise, orphaned children may be later
|
||||
// adopted by another subscription.
|
||||
// type: boolean
|
||||
// default: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The removed domain permission subscription.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionSubscriptionRemovePOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
type RemoveForm struct {
|
||||
RemoveChildren *bool `json:"remove_children" form:"remove_children"`
|
||||
}
|
||||
|
||||
form := new(RemoveForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Default removeChildren to true.
|
||||
removeChildren := util.PtrOrValue(form.RemoveChildren, true)
|
||||
|
||||
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionRemove(
|
||||
c.Request.Context(),
|
||||
id,
|
||||
removeChildren,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permSub)
|
||||
}
|
||||
177
internal/api/client/admin/domainpermissionsubscriptionsget.go
Normal file
177
internal/api/client/admin/domainpermissionsubscriptionsget.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionsGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionsGet
|
||||
//
|
||||
// View domain permission subscriptions.
|
||||
//
|
||||
// The subscriptions 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_subscriptions?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_subscriptions?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: permission_type
|
||||
// type: string
|
||||
// description: Filter on "block" or "allow" type subscriptions.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// 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: since_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: min_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: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// minimum: 1
|
||||
// maximum: 100
|
||||
// in: query
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission subscriptions.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '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) DomainPermissionSubscriptionsGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permType := c.Query(apiutil.DomainPermissionPermTypeKey)
|
||||
switch permType {
|
||||
case "", "block", "allow":
|
||||
// No problem.
|
||||
|
||||
default:
|
||||
// Invalid.
|
||||
text := fmt.Sprintf(
|
||||
"permission_type %s not recognized, valid values are empty string, block, or allow",
|
||||
permType,
|
||||
)
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGet(
|
||||
c.Request.Context(),
|
||||
gtsmodel.ParseDomainPermissionType(permType),
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionsPreviewGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions/preview domainPermissionSubscriptionsPreviewGet
|
||||
//
|
||||
// View all domain permission subscriptions of the given permission type, in priority order (highest to lowest).
|
||||
//
|
||||
// This view allows you to see the order in which domain permissions will actually be fetched and created.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: permission_type
|
||||
// type: string
|
||||
// description: Filter on "block" or "allow" type subscriptions.
|
||||
// in: query
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission subscriptions.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// '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) DomainPermissionSubscriptionsPreviewGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permType := c.Query(apiutil.DomainPermissionPermTypeKey)
|
||||
switch permType {
|
||||
case "block", "allow":
|
||||
// No problem.
|
||||
|
||||
case "":
|
||||
// Not set.
|
||||
const text = "permission_type must be set, valid values are block or allow"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
default:
|
||||
// Invalid.
|
||||
text := fmt.Sprintf(
|
||||
"permission_type %s not recognized, valid values are block or allow",
|
||||
permType,
|
||||
)
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGetByPriority(
|
||||
c.Request.Context(),
|
||||
gtsmodel.ParseDomainPermissionType(permType),
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp)
|
||||
}
|
||||
118
internal/api/client/admin/domainpermissionsubscriptiontest.go
Normal file
118
internal/api/client/admin/domainpermissionsubscriptiontest.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionTestPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions/{id}/test domainPermissionSubscriptionTest
|
||||
//
|
||||
// Test one domain permission subscription by making your instance fetch and parse it *without creating permissions*.
|
||||
//
|
||||
// The response body will be a list of domain permissions that *would* be created by this subscription, OR an error message.
|
||||
//
|
||||
// This is useful in cases where you want to check that your instance can actually fetch + parse a list.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: >-
|
||||
// Either an array of domain permissions, OR an error message of the form
|
||||
// `{"error":"[ERROR MESSAGE HERE]"}` indicating why the list could not be fetched.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domain"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionSubscriptionTestPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionTest(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
type DomainPermissionSubscriptionTestTestSuite struct {
|
||||
AdminStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionSubscriptionTestTestSuite) TestDomainPermissionSubscriptionTestCSV() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
testAccount = suite.testAccounts["admin_account"]
|
||||
permSub = >smodel.DomainPermissionSubscription{
|
||||
ID: "01JGE681TQSBPAV59GZXPKE62H",
|
||||
Priority: 255,
|
||||
Title: "whatever!",
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
AsDraft: util.Ptr(false),
|
||||
AdoptOrphans: util.Ptr(true),
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
URI: "https://lists.example.org/baddies.csv",
|
||||
ContentType: gtsmodel.DomainPermSubContentTypeCSV,
|
||||
}
|
||||
)
|
||||
|
||||
// Create a subscription for a CSV list of baddies.
|
||||
err := suite.state.DB.PutDomainPermissionSubscription(ctx, permSub)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Prepare the request to the /test endpoint.
|
||||
subPath := strings.ReplaceAll(
|
||||
admin.DomainPermissionSubscriptionTestPath,
|
||||
":id", permSub.ID,
|
||||
)
|
||||
path := "/api" + subPath
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx := suite.newContext(recorder, http.MethodPost, nil, path, "application/json")
|
||||
ginCtx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: apiutil.IDKey,
|
||||
Value: permSub.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// Trigger the handler.
|
||||
suite.adminModule.DomainPermissionSubscriptionTestPOSTHandler(ginCtx)
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// Read the body back.
|
||||
b, err := io.ReadAll(recorder.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
dst := new(bytes.Buffer)
|
||||
if err := json.Indent(dst, b, "", " "); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Ensure expected.
|
||||
suite.Equal(`[
|
||||
{
|
||||
"domain": "bumfaces.net",
|
||||
"public_comment": "big jerks"
|
||||
},
|
||||
{
|
||||
"domain": "peepee.poopoo",
|
||||
"public_comment": "harassment"
|
||||
},
|
||||
{
|
||||
"domain": "nothanks.com"
|
||||
}
|
||||
]`, dst.String())
|
||||
|
||||
// No permissions should be created
|
||||
// since this is a dry run / test.
|
||||
blocked, err := suite.state.DB.AreDomainsBlocked(
|
||||
ctx,
|
||||
[]string{"bumfaces.net", "peepee.poopoo", "nothanks.com"},
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(blocked)
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionSubscriptionTestTestSuite) TestDomainPermissionSubscriptionTestText() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
testAccount = suite.testAccounts["admin_account"]
|
||||
permSub = >smodel.DomainPermissionSubscription{
|
||||
ID: "01JGE681TQSBPAV59GZXPKE62H",
|
||||
Priority: 255,
|
||||
Title: "whatever!",
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
AsDraft: util.Ptr(false),
|
||||
AdoptOrphans: util.Ptr(true),
|
||||
CreatedByAccountID: testAccount.ID,
|
||||
CreatedByAccount: testAccount,
|
||||
URI: "https://lists.example.org/baddies.txt",
|
||||
ContentType: gtsmodel.DomainPermSubContentTypePlain,
|
||||
}
|
||||
)
|
||||
|
||||
// Create a subscription for a plaintext list of baddies.
|
||||
err := suite.state.DB.PutDomainPermissionSubscription(ctx, permSub)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Prepare the request to the /test endpoint.
|
||||
subPath := strings.ReplaceAll(
|
||||
admin.DomainPermissionSubscriptionTestPath,
|
||||
":id", permSub.ID,
|
||||
)
|
||||
path := "/api" + subPath
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx := suite.newContext(recorder, http.MethodPost, nil, path, "application/json")
|
||||
ginCtx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: apiutil.IDKey,
|
||||
Value: permSub.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// Trigger the handler.
|
||||
suite.adminModule.DomainPermissionSubscriptionTestPOSTHandler(ginCtx)
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// Read the body back.
|
||||
b, err := io.ReadAll(recorder.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
dst := new(bytes.Buffer)
|
||||
if err := json.Indent(dst, b, "", " "); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Ensure expected.
|
||||
suite.Equal(`[
|
||||
{
|
||||
"domain": "bumfaces.net"
|
||||
},
|
||||
{
|
||||
"domain": "peepee.poopoo"
|
||||
},
|
||||
{
|
||||
"domain": "nothanks.com"
|
||||
}
|
||||
]`, dst.String())
|
||||
|
||||
// No permissions should be created
|
||||
// since this is a dry run / test.
|
||||
blocked, err := suite.state.DB.AreDomainsBlocked(
|
||||
ctx,
|
||||
[]string{"bumfaces.net", "peepee.poopoo", "nothanks.com"},
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(blocked)
|
||||
}
|
||||
|
||||
func TestDomainPermissionSubscriptionTestTestSuite(t *testing.T) {
|
||||
suite.Run(t, &DomainPermissionSubscriptionTestTestSuite{})
|
||||
}
|
||||
254
internal/api/client/admin/domainpermissionsubscriptionupdate.go
Normal file
254
internal/api/client/admin/domainpermissionsubscriptionupdate.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// DomainPermissionSubscriptionPATCHHandler swagger:operation PATCH /api/v1/admin/domain_permission_subscriptions/${id} domainPermissionSubscriptionUpdate
|
||||
//
|
||||
// Update a domain permission subscription with the given parameters.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission subscription.
|
||||
// type: string
|
||||
// -
|
||||
// name: priority
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Priority of this subscription compared to others of the same permission type.
|
||||
// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
|
||||
// permissions generated by lower priority subscriptions. When two subscriptions
|
||||
// have the same `priority` value, priority is indeterminate, so it's recommended
|
||||
// to always set this value manually.
|
||||
// type: number
|
||||
// minimum: 0
|
||||
// maximum: 255
|
||||
// -
|
||||
// name: title
|
||||
// in: formData
|
||||
// description: Optional title for this subscription.
|
||||
// type: string
|
||||
// -
|
||||
// name: uri
|
||||
// in: formData
|
||||
// description: URI to call in order to fetch the permissions list.
|
||||
// type: string
|
||||
// -
|
||||
// name: as_draft
|
||||
// in: formData
|
||||
// description: >-
|
||||
// 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.
|
||||
// Defaults to "true".
|
||||
// type: boolean
|
||||
// default: true
|
||||
// -
|
||||
// name: adopt_orphans
|
||||
// in: formData
|
||||
// description: >-
|
||||
// If true, this domain permission subscription will "adopt" domain permissions
|
||||
// which already exist on the instance, and which meet the following conditions:
|
||||
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
|
||||
// in the subscribed list. Such orphaned domain permissions will be given this
|
||||
// subscription's subscription ID value and be managed by this subscription.
|
||||
// type: boolean
|
||||
// default: false
|
||||
// -
|
||||
// name: content_type
|
||||
// in: formData
|
||||
// description: >-
|
||||
// MIME content type to use when parsing the permissions list.
|
||||
// One of "text/plain", "text/csv", and "application/json".
|
||||
// type: string
|
||||
// -
|
||||
// name: fetch_username
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Optional basic auth username to provide when fetching given uri.
|
||||
// If set, will be transmitted along with `fetch_password` when doing the fetch.
|
||||
// type: string
|
||||
// -
|
||||
// name: fetch_password
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Optional basic auth password to provide when fetching given uri.
|
||||
// If set, will be transmitted along with `fetch_username` when doing the fetch.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The updated domain permission subscription.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermissionSubscription"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionSubscriptionPATCHHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse + validate form.
|
||||
form := new(apimodel.DomainPermissionSubscriptionRequest)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize priority if set.
|
||||
var priority *uint8
|
||||
if form.Priority != nil {
|
||||
prioInt := *form.Priority
|
||||
if prioInt < 0 || prioInt > 255 {
|
||||
const errText = "priority must be a number in the range 0 to 255"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
priority = util.Ptr(uint8(prioInt)) // #nosec G115 -- Just validated.
|
||||
}
|
||||
|
||||
// Validate URI if set.
|
||||
var uriStr *string
|
||||
if form.URI != nil {
|
||||
uri, err := url.Parse(*form.URI)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid uri provided: %w", err)
|
||||
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize URI by converting back to string.
|
||||
uriStr = util.Ptr(uri.String())
|
||||
}
|
||||
|
||||
// Validate content type if set.
|
||||
var contentType *gtsmodel.DomainPermSubContentType
|
||||
if form.ContentType != nil {
|
||||
ct, errWithCode := parseDomainPermSubContentType(*form.ContentType)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType = &ct
|
||||
}
|
||||
|
||||
// Make sure at least one field is set,
|
||||
// otherwise we're trying to update nothing.
|
||||
if priority == nil &&
|
||||
form.Title == nil &&
|
||||
uriStr == nil &&
|
||||
contentType == nil &&
|
||||
form.AsDraft == nil &&
|
||||
form.AdoptOrphans == nil &&
|
||||
form.FetchUsername == nil &&
|
||||
form.FetchPassword == nil {
|
||||
const errText = "no updateable fields set on request"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionUpdate(
|
||||
c.Request.Context(),
|
||||
id,
|
||||
priority,
|
||||
form.Title,
|
||||
uriStr,
|
||||
contentType,
|
||||
form.AsDraft,
|
||||
form.AdoptOrphans,
|
||||
form.FetchUsername,
|
||||
form.FetchPassword,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permSub)
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ import (
|
|||
// The code to use for the emoji, which will be used by instance denizens to select it.
|
||||
// This must be unique on the instance.
|
||||
// type: string
|
||||
// pattern: \w{2,30}
|
||||
// pattern: \w{1,30}
|
||||
// required: true
|
||||
// -
|
||||
// name: image
|
||||
|
|
@ -145,8 +145,8 @@ func validateCreateEmoji(form *apimodel.EmojiCreateRequest) error {
|
|||
return errors.New("no emoji given")
|
||||
}
|
||||
|
||||
maxSize := config.GetMediaEmojiLocalMaxSize()
|
||||
if form.Image.Size > int64(maxSize) {
|
||||
maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
|
||||
if form.Image.Size > maxSize {
|
||||
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import (
|
|||
// The code to use for the emoji, which will be used by instance denizens to select it.
|
||||
// This must be unique on the instance. Works for the `copy` action type only.
|
||||
// type: string
|
||||
// pattern: \w{2,30}
|
||||
// pattern: \w{1,30}
|
||||
// -
|
||||
// name: image
|
||||
// in: formData
|
||||
|
|
@ -208,8 +208,8 @@ func validateUpdateEmoji(form *apimodel.EmojiUpdateRequest) error {
|
|||
}
|
||||
|
||||
if hasImage {
|
||||
maxSize := config.GetMediaEmojiLocalMaxSize()
|
||||
if form.Image.Size > int64(maxSize) {
|
||||
maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
|
||||
if form.Image.Size > maxSize {
|
||||
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 1 and 30 characters, letters, numbers, and underscores only"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() {
|
||||
|
|
|
|||
|
|
@ -183,10 +183,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -228,10 +229,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -286,10 +288,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -340,10 +343,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -407,10 +411,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -465,10 +470,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -479,6 +485,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
|
@ -512,10 +519,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
|
@ -657,10 +665,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -715,10 +724,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -729,6 +739,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
|
@ -762,10 +773,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
|
@ -907,10 +919,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -965,10 +978,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -979,6 +993,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
|
@ -1012,10 +1027,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
|
|
|||
42
internal/api/client/announcements/announcements.go
Normal file
42
internal/api/client/announcements/announcements.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package announcements
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
)
|
||||
|
||||
// BasePath is the base path for this api module, excluding the api prefix
|
||||
const BasePath = "/v1/announcements"
|
||||
|
||||
type Module struct {
|
||||
processor *processing.Processor
|
||||
}
|
||||
|
||||
func New(processor *processing.Processor) *Module {
|
||||
return &Module{
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, BasePath, m.AnnouncementsGETHandler)
|
||||
}
|
||||
74
internal/api/client/announcements/announcementsget.go
Normal file
74
internal/api/client/announcements/announcementsget.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// 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 announcements
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AnnouncementsGETHandler swagger:operation GET /api/v1/announcements announcementsGet
|
||||
//
|
||||
// Get an array of currently active announcements.
|
||||
//
|
||||
// THIS ENDPOINT IS CURRENTLY NOT FULLY IMPLEMENTED: it will always return an empty array.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - announcements
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:announcements
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// type: object
|
||||
// maxItems: 0
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AnnouncementsGETHandler(c *gin.Context) {
|
||||
_, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), 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
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiutil.EmptyJSONArray)
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -95,6 +96,7 @@ func (suite *BookmarkTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ Cool Ass Posters From This Instance,admin@localhost:8080
|
|||
"media_storage": "",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"statuses_count": 9,
|
||||
"lists_count": 1,
|
||||
"blocks_count": 0,
|
||||
"mutes_count": 0
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package favourites_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -79,6 +80,7 @@ func (suite *FavouritesStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -90,6 +91,7 @@ func (suite *FiltersTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func (suite *FiltersTestSuite) postFilter(
|
|||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
expiresInStr *string,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
|
|
@ -75,6 +76,8 @@ func (suite *FiltersTestSuite) postFilter(
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
|||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -155,7 +158,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
|||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -182,7 +185,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -203,7 +206,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() {
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -211,7 +214,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -220,7 +223,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -228,7 +231,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -237,8 +240,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
|||
// There should be a filter with this phrase as its title in our test fixtures. Creating another should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
phrase := "fnord"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// postFilterWithExpiration creates a filter with optional expiration.
|
||||
func (suite *FiltersTestSuite) postFilterWithExpiration(phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() {
|
||||
title := "Form Sins"
|
||||
expiresInStr := ""
|
||||
filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() {
|
||||
requestJson := `{
|
||||
"phrase": "JSON Sins",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ func (suite *FiltersTestSuite) putFilter(
|
|||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
expiresInStr *string,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
|
|
@ -76,6 +77,8 @@ func (suite *FiltersTestSuite) putFilter(
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +131,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
|||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -160,7 +163,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
|||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -188,7 +191,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -210,7 +213,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -219,7 +222,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterMissingPhrase() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -229,7 +232,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -238,7 +241,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterMissingContext() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -248,7 +251,7 @@ func (suite *FiltersTestSuite) TestPutFilterMissingContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "metasyntactic variables"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -258,7 +261,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
|||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -268,8 +271,60 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
|||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// setFilterExpiration sets filter expiration.
|
||||
func (suite *FiltersTestSuite) setFilterExpiration(id string, phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() {
|
||||
filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
id := filterKeyword.ID
|
||||
phrase := filterKeyword.Keyword
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to an empty string.
|
||||
expiresInStr := ""
|
||||
filter = suite.setFilterExpiration(id, &phrase, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() {
|
||||
filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
id := filterKeyword.ID
|
||||
phrase := filterKeyword.Keyword
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to a null literal.
|
||||
requestJson := `{
|
||||
"phrase": "fnord",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,14 @@ package v1
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1) error {
|
||||
func validateNormalizeCreateUpdateFilter(form *apimodel.FilterCreateUpdateRequestV1) error {
|
||||
if err := validate.FilterKeyword(form.Phrase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -47,24 +46,16 @@ func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1
|
|||
return errors.New("irreversible aka server-side drop filters are not supported yet")
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -90,6 +91,7 @@ func (suite *FiltersTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -227,24 +225,16 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
|||
// Apply defaults for missing fields.
|
||||
form.FilterAction = util.Ptr(action)
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesKeyword *[]string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
|
|
@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, acti
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
if keywordsAttributesKeyword != nil {
|
||||
ctx.Request.Form["keywords_attributes[][keyword]"] = *keywordsAttributesKeyword
|
||||
|
|
@ -130,7 +132,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
|||
keywordsAttributesWholeWord := []bool{true, false}
|
||||
// Checked in lexical order by status ID, so keep this sorted.
|
||||
statusAttributesStatusID := []string{"01HEN2QRFA8H3C6QPN7RD4KSR6", "01HEWV37MHV8BAC8ANFGVRRM5D"}
|
||||
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, nil, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "", &keywordsAttributesKeyword)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -197,7 +199,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
|||
}
|
||||
]
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -245,7 +247,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -267,7 +269,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -275,7 +277,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -284,7 +286,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -292,7 +294,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
title := "GNU/Linux"
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -301,8 +303,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
|||
// Creating another filter with the same title should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
title := suite.testFilters["local_account_1_filter_1"].Title
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// postFilterWithExpiration creates a filter with optional expiration.
|
||||
func (suite *FiltersTestSuite) postFilterWithExpiration(title *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(title, &context, nil, expiresIn, expiresInStr, nil, nil, requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() {
|
||||
title := "Form Crimes"
|
||||
expiresInStr := ""
|
||||
filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() {
|
||||
requestJson := `{
|
||||
"title": "JSON Crimes",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ package v2
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -271,24 +269,16 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesID *[]string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesID *[]string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
|
|
@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
if keywordsAttributesID != nil {
|
||||
ctx.Request.Form["keywords_attributes[][id]"] = *keywordsAttributesID
|
||||
|
|
@ -159,7 +161,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
|||
keywordsAttributesWholeWord := []bool{true, false, true}
|
||||
keywordsAttributesDestroy := []bool{false, true}
|
||||
statusesAttributesStatusID := []string{suite.testStatuses["remote_account_1_status_2"].ID}
|
||||
filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, &keywordsAttributesID, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, nil, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "", &keywordsAttributesID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -231,7 +233,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
|||
}
|
||||
]
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -281,7 +283,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -302,7 +304,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -312,7 +314,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -322,7 +324,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := suite.testFilters["local_account_1_filter_2"].Title
|
||||
_, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`)
|
||||
_, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -332,7 +334,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
|||
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -342,8 +344,70 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
|||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// setFilterExpiration sets filter expiration.
|
||||
func (suite *FiltersTestSuite) setFilterExpiration(id string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 {
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, expiresIn, expiresInStr, nil, nil, nil, nil, nil, nil, requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() {
|
||||
id := suite.testFilters["local_account_1_filter_2"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to an empty string.
|
||||
expiresInStr := ""
|
||||
filter = suite.setFilterExpiration(id, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() {
|
||||
id := suite.testFilters["local_account_1_filter_3"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to a null literal.
|
||||
requestJson := `{
|
||||
"expires_in": null
|
||||
}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnalteredExpirationDateJSON() {
|
||||
id := suite.testFilters["local_account_1_filter_4"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Update nothing. There should still be an expiration date.
|
||||
requestJson := `{}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, &requestJson)
|
||||
suite.NotNil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/followedtags"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -79,6 +80,7 @@ func (suite *FollowedTagsTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -82,6 +83,7 @@ func (suite *FollowRequestStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -97,10 +97,11 @@ func (suite *GetTestSuite) TestGet() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2023-11-02T10:44:25.000Z",
|
||||
"last_status_at": "2023-11-02",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -84,6 +85,7 @@ func (suite *InstanceStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ func validateInstanceUpdate(form *apimodel.InstanceSettingsUpdateRequest) error
|
|||
form.ContactEmail == nil &&
|
||||
form.ShortDescription == nil &&
|
||||
form.Description == nil &&
|
||||
form.CustomCSS == nil &&
|
||||
form.Terms == nil &&
|
||||
form.Avatar == nil &&
|
||||
form.AvatarDescription == nil &&
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
|
@ -51,6 +52,7 @@ func (suite *InstancePatchTestSuite) instancePatch(fieldName string, fileName st
|
|||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, requestBody.Bytes(), w.FormDataContentType(), true)
|
||||
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
middleware.Logger(false)(ctx)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
|
@ -80,7 +82,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "Example Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -113,6 +115,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -120,7 +123,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -130,10 +133,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -155,7 +158,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
@ -174,10 +177,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -220,7 +224,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "Geoff's Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -253,6 +257,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -260,7 +265,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -270,10 +275,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -295,7 +300,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
@ -314,10 +319,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -360,7 +366,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "GoToSocial Testrig Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -393,6 +399,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -400,7 +407,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -410,10 +417,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -435,7 +442,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
@ -454,10 +461,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -551,7 +559,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "GoToSocial Testrig Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -584,6 +592,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -591,7 +600,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -601,10 +610,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -626,7 +635,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
@ -645,10 +654,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -713,7 +723,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "GoToSocial Testrig Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -746,6 +756,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -753,7 +764,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -763,10 +774,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -788,7 +799,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
|
||||
|
|
@ -811,10 +822,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
@ -858,7 +870,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
"static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+`
|
||||
"thumbnail_static_type": "image/webp",
|
||||
"thumbnail_description": "A bouncing little green peglin.",
|
||||
"blurhash": "LE9801Rl4Yt5%dWCV]t5Dmoex?WC"
|
||||
"blurhash": "LF9Hm*Rl4Yt5.4RlRSt5IXkBxsj["
|
||||
}`, string(instanceV2ThumbnailJson))
|
||||
|
||||
// double extra special bonus: now update the image description without changing the image
|
||||
|
|
@ -894,7 +906,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"uri": "http://localhost:8080",
|
||||
"uri": "localhost:8080",
|
||||
"account_domain": "localhost:8080",
|
||||
"title": "GoToSocial Testrig Instance",
|
||||
"description": "<p>Here's a fuller description of the GoToSocial testrig instance.</p><p>This instance is for testing purposes only. It doesn't federate at all. Go check out <a href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/tree/main/testrig</a> and <a href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing</a></p><p>Users on this instance:</p><ul><li><span class=\"h-card\"><a href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>admin</span></a></span> (admin!).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> (posts about turtles, we don't know why).</li><li><span class=\"h-card\"><a href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>the_mighty_zork</span></a></span> (who knows).</li></ul><p>If you need to edit the models for the testrig, you can do so at <code>internal/testmodels.go</code>.</p>",
|
||||
|
|
@ -927,6 +939,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
"image/webp",
|
||||
"audio/mp2",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"video/x-msvideo",
|
||||
"audio/flac",
|
||||
"audio/x-flac",
|
||||
|
|
@ -934,7 +947,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
"image/apng",
|
||||
"audio/ogg",
|
||||
"video/ogg",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"audio/x-ms-wma",
|
||||
|
|
@ -944,10 +957,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
"video/x-matroska"
|
||||
],
|
||||
"image_size_limit": 41943040,
|
||||
"image_matrix_limit": 16777216,
|
||||
"image_matrix_limit": 2147483647,
|
||||
"video_size_limit": 41943040,
|
||||
"video_frame_rate_limit": 60,
|
||||
"video_matrix_limit": 16777216
|
||||
"video_frame_rate_limit": 2147483647,
|
||||
"video_matrix_limit": 2147483647
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 6,
|
||||
|
|
@ -969,7 +982,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 20,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
@ -988,10 +1001,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"last_status_at": "2021-10-20",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package lists_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/lists"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -85,6 +86,7 @@ func (suite *ListsStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/mutes"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -81,6 +82,7 @@ func (suite *MutesTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ func (suite *MutesTestSuite) TestIndefinitelyMutedAccountSerializesMuteExpiratio
|
|||
|
||||
// Fetch all muted accounts for the logged-in account.
|
||||
// The expected body contains `"mute_expires_at":null`.
|
||||
_, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","followers_count":0,"following_count":0,"statuses_count":3,"last_status_at":"2021-09-11T09:40:37.000Z","emojis":[],"fields":[],"mute_expires_at":null}]`)
|
||||
_, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","header_description":"Flat gray background (default header).","followers_count":0,"following_count":0,"statuses_count":4,"last_status_at":"2024-11-01","emojis":[],"fields":[],"mute_expires_at":null}]`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package notifications_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/notifications"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -81,6 +82,7 @@ func (suite *NotificationsTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// NotificationsGETHandler swagger:operation GET /api/v1/notifications notifications
|
||||
|
|
@ -152,27 +154,23 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
limit := 20
|
||||
limitString := c.Query(LimitKey)
|
||||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 32)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
page, errWithCode := paging.ParseIDPage(c,
|
||||
1, // min limit
|
||||
80, // max limit
|
||||
20, // no limit
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
resp, errWithCode := m.processor.Timeline().NotificationsGet(
|
||||
c.Request.Context(),
|
||||
ctx,
|
||||
authed,
|
||||
c.Query(MaxIDKey),
|
||||
c.Query(SinceIDKey),
|
||||
c.Query(MinIDKey),
|
||||
limit,
|
||||
c.QueryArray(TypesKey),
|
||||
c.QueryArray(ExcludeTypesKey),
|
||||
page,
|
||||
parseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types.
|
||||
parseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
|
|
@ -185,3 +183,28 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
|
||||
// parseNotificationTypes converts the given slice of string values
|
||||
// to gtsmodel notification types, logging + skipping unknown types.
|
||||
func parseNotificationTypes(
|
||||
ctx context.Context,
|
||||
values []string,
|
||||
) []gtsmodel.NotificationType {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ntypes := make([]gtsmodel.NotificationType, 0, len(values))
|
||||
for _, value := range values {
|
||||
ntype := gtsmodel.ParseNotificationType(value)
|
||||
if ntype == gtsmodel.NotificationUnknown {
|
||||
// Type we don't know about (yet), log and ignore it.
|
||||
log.Warnf(ctx, "ignoring unknown type %s", value)
|
||||
continue
|
||||
}
|
||||
|
||||
ntypes = append(ntypes, ntype)
|
||||
}
|
||||
|
||||
return ntypes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,45 @@ func (suite *NotificationsTestSuite) TestGetNotificationsIncludeOneType() {
|
|||
}
|
||||
}
|
||||
|
||||
// Test including an unknown notification type, it should be ignored.
|
||||
func (suite *NotificationsTestSuite) TestGetNotificationsIncludeUnknownType() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
testToken := suite.testTokens["local_account_1"]
|
||||
testUser := suite.testUsers["local_account_1"]
|
||||
|
||||
suite.addMoreNotifications(testAccount)
|
||||
|
||||
maxID := ""
|
||||
minID := ""
|
||||
limit := 10
|
||||
types := []string{"favourite", "something.weird"}
|
||||
excludeTypes := []string(nil)
|
||||
expectedHTTPStatus := http.StatusOK
|
||||
expectedBody := ""
|
||||
|
||||
notifications, _, err := suite.getNotifications(
|
||||
testAccount,
|
||||
testToken,
|
||||
testUser,
|
||||
maxID,
|
||||
minID,
|
||||
limit,
|
||||
types,
|
||||
excludeTypes,
|
||||
expectedHTTPStatus,
|
||||
expectedBody,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// This should only include the fav notification.
|
||||
suite.Len(notifications, 1)
|
||||
for _, notification := range notifications {
|
||||
suite.Equal("favourite", notification.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookmarkTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(NotificationsTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package polls_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/polls"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -76,6 +77,7 @@ func (suite *PollsStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -127,10 +127,11 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package reports_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/reports"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -76,6 +77,7 @@ func (suite *ReportsStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -153,10 +153,11 @@ func (suite *ReportsGetTestSuite) TestGetReports() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -243,10 +244,11 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -317,10 +319,11 @@ func (suite *ReportsGetTestSuite) TestGetReports6() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
@ -375,10 +378,11 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/search"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -80,6 +81,7 @@ func (suite *SearchStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -916,7 +916,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 5)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
|
@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 2)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
|
@ -1002,7 +1002,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,9 +83,10 @@ func New(processor *processing.Processor) *Module {
|
|||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
// create / get / delete status
|
||||
// create / get / edit / delete status
|
||||
attachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler)
|
||||
attachHandler(http.MethodGet, BasePathWithID, m.StatusGETHandler)
|
||||
attachHandler(http.MethodPut, BasePathWithID, m.StatusEditPUTHandler)
|
||||
attachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler)
|
||||
|
||||
// fave stuff
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -192,6 +193,7 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -145,6 +146,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() {
|
|||
"card": null,
|
||||
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [
|
||||
{
|
||||
"category": "reactions",
|
||||
|
|
@ -280,6 +282,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -329,6 +332,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
|||
"card": null,
|
||||
"content": "hi!",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -494,6 +498,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -539,6 +544,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -591,7 +597,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
},
|
||||
"reblogged": true,
|
||||
"reblogs_count": 0,
|
||||
|
|
@ -601,7 +607,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"tags": [],
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
|
|
|
|||
|
|
@ -21,18 +21,15 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/form/v4"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
// StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate
|
||||
|
|
@ -181,7 +178,7 @@ import (
|
|||
// Providing this parameter will cause ScheduledStatus to be returned instead of Status.
|
||||
// Must be at least 5 minutes in the future.
|
||||
//
|
||||
// This feature isn't implemented yet.
|
||||
// This feature isn't implemented yet; attemping to set it will return 501 Not Implemented.
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
|
|
@ -254,6 +251,8 @@ import (
|
|||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
// '501':
|
||||
// description: scheduled_at was set, but this feature is not yet implemented
|
||||
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
|
|
@ -271,9 +270,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
form, err := parseStatusCreateForm(c)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
form, errWithCode := parseStatusCreateForm(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -286,11 +285,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
// }
|
||||
// form.Status += "\n\nsent from " + user + "'s iphone\n"
|
||||
|
||||
if err := validateNormalizeCreateStatus(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, errWithCode := m.processor.Status().Create(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
|
|
@ -302,7 +296,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiStatus)
|
||||
apiutil.JSON(c, http.StatusOK, apiStatus)
|
||||
}
|
||||
|
||||
// intPolicyFormBinding satisfies gin's binding.Binding interface.
|
||||
|
|
@ -327,93 +321,69 @@ func (intPolicyFormBinding) Bind(req *http.Request, obj any) error {
|
|||
return decoder.Decode(obj, req.Form)
|
||||
}
|
||||
|
||||
func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error) {
|
||||
func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, gtserror.WithCode) {
|
||||
form := new(apimodel.StatusCreateRequest)
|
||||
|
||||
switch ct := c.ContentType(); ct {
|
||||
case binding.MIMEJSON:
|
||||
// Just bind with default json binding.
|
||||
if err := c.ShouldBindWith(form, binding.JSON); err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
case binding.MIMEPOSTForm:
|
||||
// Bind with default form binding first.
|
||||
if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
// Now do custom binding.
|
||||
intReqForm := new(apimodel.StatusInteractionPolicyForm)
|
||||
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
form.InteractionPolicy = intReqForm.InteractionPolicy
|
||||
|
||||
case binding.MIMEMultipartPOSTForm:
|
||||
// Bind with default form binding first.
|
||||
if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
// Now do custom binding.
|
||||
intReqForm := new(apimodel.StatusInteractionPolicyForm)
|
||||
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
form.InteractionPolicy = intReqForm.InteractionPolicy
|
||||
|
||||
default:
|
||||
err := fmt.Errorf(
|
||||
"content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
|
||||
ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm,
|
||||
)
|
||||
return nil, err
|
||||
text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
|
||||
ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
|
||||
return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text)
|
||||
}
|
||||
|
||||
return form, nil
|
||||
}
|
||||
|
||||
// validateNormalizeCreateStatus checks the form
|
||||
// for disallowed combinations of attachments and
|
||||
// overlength inputs.
|
||||
//
|
||||
// Side effect: normalizes the post's language tag.
|
||||
func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error {
|
||||
hasStatus := form.Status != ""
|
||||
hasMedia := len(form.MediaIDs) != 0
|
||||
hasPoll := form.Poll != nil
|
||||
|
||||
if !hasStatus && !hasMedia && !hasPoll {
|
||||
return errors.New("no status, media, or poll provided")
|
||||
}
|
||||
|
||||
if hasMedia && hasPoll {
|
||||
return errors.New("can't post media + poll in same status")
|
||||
}
|
||||
|
||||
maxChars := config.GetStatusesMaxChars()
|
||||
if length := len([]rune(form.Status)) + len([]rune(form.SpoilerText)); length > maxChars {
|
||||
return fmt.Errorf("status too long, %d characters provided (including spoiler/content warning) but limit is %d", length, maxChars)
|
||||
}
|
||||
|
||||
maxMediaFiles := config.GetStatusesMediaMaxFiles()
|
||||
if len(form.MediaIDs) > maxMediaFiles {
|
||||
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
|
||||
}
|
||||
|
||||
if form.Poll != nil {
|
||||
if err := validateNormalizeCreatePoll(form); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if form.Language != "" {
|
||||
language, err := validate.Language(form.Language)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Language = language
|
||||
// Check not scheduled status.
|
||||
if form.ScheduledAt != "" {
|
||||
const text = "scheduled_at is not yet implemented"
|
||||
return nil, gtserror.NewErrorNotImplemented(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Check if the deprecated "federated" field was
|
||||
|
|
@ -422,47 +392,20 @@ func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error {
|
|||
form.LocalOnly = util.Ptr(!*form.Federated) // nolint:staticcheck
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
// Normalize poll expiry time if a poll was given.
|
||||
if form.Poll != nil && form.Poll.ExpiresInI != nil {
|
||||
|
||||
func validateNormalizeCreatePoll(form *apimodel.StatusCreateRequest) error {
|
||||
maxPollOptions := config.GetStatusesPollMaxOptions()
|
||||
maxPollChars := config.GetStatusesPollOptionMaxChars()
|
||||
|
||||
// Normalize poll expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.Poll.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.Poll.ExpiresIn = int(e)
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.Poll.ExpiresIn = expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
expiresIn, err := apiutil.ParseDuration(
|
||||
form.Poll.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
form.Poll.ExpiresIn = util.PtrOrZero(expiresIn)
|
||||
}
|
||||
|
||||
if len(form.Poll.Options) == 0 {
|
||||
return errors.New("poll with no options")
|
||||
}
|
||||
|
||||
if len(form.Poll.Options) > maxPollOptions {
|
||||
return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), maxPollOptions)
|
||||
}
|
||||
|
||||
for _, p := range form.Poll.Options {
|
||||
if length := len([]rune(p)); length > maxPollChars {
|
||||
return fmt.Errorf("poll option too long, %d characters provided but limit is %d", length, maxPollChars)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return form, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -187,6 +188,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicy() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -282,6 +284,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicyJSON() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -365,6 +368,25 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {
|
|||
}`, out)
|
||||
}
|
||||
|
||||
func (suite *StatusCreateTestSuite) TestPostNewScheduledStatus() {
|
||||
out, recorder := suite.postStatus(map[string][]string{
|
||||
"status": {"this is a brand new status! #helloworld"},
|
||||
"spoiler_text": {"hello hello"},
|
||||
"sensitive": {"true"},
|
||||
"visibility": {string(apimodel.VisibilityMutualsOnly)},
|
||||
"scheduled_at": {"2080-10-04T15:32:02.018Z"},
|
||||
}, "")
|
||||
|
||||
// We should have 501 from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusNotImplemented, recorder.Code)
|
||||
|
||||
// We should have a helpful error message.
|
||||
suite.Equal(`{
|
||||
"error": "Not Implemented: scheduled_at is not yet implemented"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
|
||||
out, recorder := suite.postStatus(map[string][]string{
|
||||
"status": {statusMarkdown},
|
||||
|
|
@ -388,6 +410,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
|
|||
"card": null,
|
||||
"content": "<h1>Title</h1><h2>Smaller title</h2><p>This is a post written in <a href=\"https://www.markdownguide.org/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">markdown</a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -471,6 +494,7 @@ func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() {
|
|||
"card": null,
|
||||
"content": "<p>hello <span class=\"h-card\"><a href=\"https://unknown-instance.com/@brand_new_person\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>brand_new_person</span></a></span></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -548,6 +572,7 @@ func (suite *StatusCreateTestSuite) TestPostStatusWithLinksAndTags() {
|
|||
"card": null,
|
||||
"content": "<p><a href=\"http://localhost:8080/tags/test\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>test</span></a> alright, should be able to post <a href=\"http://localhost:8080/tags/links\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>links</span></a> with fragments in them now, let's see........<br><br><a href=\"https://docs.gotosocial.org/en/latest/user_guide/posts/#links\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://docs.gotosocial.org/en/latest/user_guide/posts/#links</a><br><br><a href=\"http://localhost:8080/tags/gotosocial\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>gotosocial</span></a><br><br>(tobi remember to pull the docker image challenge)</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -631,6 +656,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
|
|||
"card": null,
|
||||
"content": "<p>here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow:<br>here's an emoji that isn't in the db: :test_emoji:</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [
|
||||
{
|
||||
"category": "reactions",
|
||||
|
|
@ -728,6 +754,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
|
|||
"card": null,
|
||||
"content": "<p>hello <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> this reply should work!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -810,6 +837,7 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
|
|||
"card": null,
|
||||
"content": "<p>here's an image attachment</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -914,6 +942,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithNoncanonicalLanguageTag
|
|||
"card": null,
|
||||
"content": "<p>English? what's English? i speak American</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -988,6 +1017,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollForm() {
|
|||
"card": null,
|
||||
"content": "<p>this is a status with a poll!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
@ -1084,6 +1114,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollJSON() {
|
|||
"card": null,
|
||||
"content": "<p>this is a status with a poll!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
|
|||
|
|
@ -95,5 +95,5 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiStatus)
|
||||
apiutil.JSON(c, http.StatusOK, apiStatus)
|
||||
}
|
||||
|
|
|
|||
249
internal/api/client/statuses/statusedit.go
Normal file
249
internal/api/client/statuses/statusedit.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
// 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 statuses
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// StatusEditPUTHandler swagger:operation PUT /api/v1/statuses statusEdit
|
||||
//
|
||||
// Edit an existing status using the given form field parameters.
|
||||
//
|
||||
// The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - statuses
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: status
|
||||
// x-go-name: Status
|
||||
// description: |-
|
||||
// Text content of the status.
|
||||
// If media_ids is provided, this becomes optional.
|
||||
// Attaching a poll is optional while status is provided.
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
// name: media_ids
|
||||
// x-go-name: MediaIDs
|
||||
// description: |-
|
||||
// Array of Attachment ids to be attached as media.
|
||||
// If provided, status becomes optional, and poll cannot be used.
|
||||
//
|
||||
// If the status is being submitted as a form, the key is 'media_ids[]',
|
||||
// but if it's json or xml, the key is 'media_ids'.
|
||||
// type: array
|
||||
// items:
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
// name: poll[options][]
|
||||
// x-go-name: PollOptions
|
||||
// description: |-
|
||||
// Array of possible poll answers.
|
||||
// If provided, media_ids cannot be used, and poll[expires_in] must be provided.
|
||||
// type: array
|
||||
// items:
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
// name: poll[expires_in]
|
||||
// x-go-name: PollExpiresIn
|
||||
// description: |-
|
||||
// Duration the poll should be open, in seconds.
|
||||
// If provided, media_ids cannot be used, and poll[options] must be provided.
|
||||
// type: integer
|
||||
// format: int64
|
||||
// in: formData
|
||||
// -
|
||||
// name: poll[multiple]
|
||||
// x-go-name: PollMultiple
|
||||
// description: Allow multiple choices on this poll.
|
||||
// type: boolean
|
||||
// default: false
|
||||
// in: formData
|
||||
// -
|
||||
// name: poll[hide_totals]
|
||||
// x-go-name: PollHideTotals
|
||||
// description: Hide vote counts until the poll ends.
|
||||
// type: boolean
|
||||
// default: true
|
||||
// in: formData
|
||||
// -
|
||||
// name: sensitive
|
||||
// x-go-name: Sensitive
|
||||
// description: Status and attached media should be marked as sensitive.
|
||||
// type: boolean
|
||||
// in: formData
|
||||
// -
|
||||
// name: spoiler_text
|
||||
// x-go-name: SpoilerText
|
||||
// description: |-
|
||||
// Text to be shown as a warning or subject before the actual content.
|
||||
// Statuses are generally collapsed behind this field.
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
// name: language
|
||||
// x-go-name: Language
|
||||
// description: ISO 639 language code for this status.
|
||||
// type: string
|
||||
// in: formData
|
||||
// -
|
||||
// name: content_type
|
||||
// x-go-name: ContentType
|
||||
// description: Content type to use when parsing this status.
|
||||
// type: string
|
||||
// enum:
|
||||
// - text/plain
|
||||
// - text/markdown
|
||||
// in: formData
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:statuses
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: "The latest status revision."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/status"
|
||||
// '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) StatusEditPUTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form, errWithCode := parseStatusEditForm(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, errWithCode := m.processor.Status().Edit(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
c.Param(IDKey),
|
||||
form,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiStatus)
|
||||
}
|
||||
|
||||
func parseStatusEditForm(c *gin.Context) (*apimodel.StatusEditRequest, gtserror.WithCode) {
|
||||
form := new(apimodel.StatusEditRequest)
|
||||
|
||||
switch ct := c.ContentType(); ct {
|
||||
case binding.MIMEJSON:
|
||||
// Just bind with default json binding.
|
||||
if err := c.ShouldBindWith(form, binding.JSON); err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
case binding.MIMEPOSTForm:
|
||||
// Bind with default form binding first.
|
||||
if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
case binding.MIMEMultipartPOSTForm:
|
||||
// Bind with default form binding first.
|
||||
if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(
|
||||
err,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
|
||||
ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
|
||||
return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Normalize poll expiry time if a poll was given.
|
||||
if form.Poll != nil && form.Poll.ExpiresInI != nil {
|
||||
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
expiresIn, err := apiutil.ParseDuration(
|
||||
form.Poll.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
form.Poll.ExpiresIn = util.PtrOrZero(expiresIn)
|
||||
}
|
||||
|
||||
return form, nil
|
||||
|
||||
}
|
||||
32
internal/api/client/statuses/statusedit_test.go
Normal file
32
internal/api/client/statuses/statusedit_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// 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 statuses_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type StatusEditTestSuite struct {
|
||||
StatusStandardTestSuite
|
||||
}
|
||||
|
||||
func TestStatusEditTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusEditTestSuite))
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
|
|
@ -104,6 +105,7 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
|
|||
"card": null,
|
||||
"content": "🐕🐕🐕🐕🐕",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
|
|
@ -185,13 +187,24 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
|||
// Fave a status that's pending approval by us.
|
||||
func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_2"]
|
||||
user = suite.testUsers["local_account_2"]
|
||||
account = suite.testAccounts["local_account_2"]
|
||||
visFilter = visibility.NewFilter(&suite.state)
|
||||
)
|
||||
|
||||
// Check visibility of status to public before posting fave.
|
||||
visible, err := visFilter.StatusVisible(ctx, nil, targetStatus)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if visible {
|
||||
suite.FailNow("status should not be visible yet")
|
||||
}
|
||||
|
||||
out, recorder := suite.postStatusFave(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
|
|
@ -216,6 +229,7 @@ func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
|
|
@ -268,30 +282,40 @@ func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
|||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
// longer be pending approval.
|
||||
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||
context.Background(),
|
||||
ctx,
|
||||
targetStatus.ID,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(*dbStatus.PendingApproval)
|
||||
suite.NotEmpty(dbStatus.ApprovedByURI)
|
||||
|
||||
// There should be an Accept
|
||||
// stored for the target status.
|
||||
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||
context.Background(), targetStatus.URI,
|
||||
ctx, targetStatus.URI,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotZero(intReq.AcceptedAt)
|
||||
suite.NotEmpty(intReq.URI)
|
||||
|
||||
// Check visibility of status to public after posting fave.
|
||||
visible, err = visFilter.StatusVisible(ctx, nil, dbStatus)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if !visible {
|
||||
suite.FailNow("status should be visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -109,13 +109,15 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() {
|
|||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
|
||||
"avatar_description": "a green goblin looking nasty",
|
||||
"avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
|
||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
|
||||
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10T09:24:00.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
|
@ -127,13 +128,15 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
|
||||
"avatar_description": "a green goblin looking nasty",
|
||||
"avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
|
||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
|
||||
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10T09:24:00.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
@ -176,6 +179,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
|
@ -212,13 +216,15 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
|
||||
"avatar_description": "a green goblin looking nasty",
|
||||
"avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S",
|
||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
|
||||
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10T09:24:00.000Z",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func (suite *StatusSourceTestSuite) TestGetSource() {
|
|||
|
||||
suite.Equal(`{
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"text": "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\nYou can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\nhello everyone!",
|
||||
"text": "hello everyone!",
|
||||
"spoiler_text": "introduction post"
|
||||
}`, dst.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import (
|
|||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var pingMsg = []byte("ping!")
|
||||
|
||||
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
|
||||
//
|
||||
// Initiate a websocket connection for live streaming of statuses and notifications.
|
||||
|
|
@ -389,40 +391,57 @@ func (m *Module) writeToWSConn(
|
|||
) {
|
||||
for {
|
||||
// Wrap context with timeout to send a ping.
|
||||
pingctx, cncl := context.WithTimeout(ctx, ping)
|
||||
pingCtx, cncl := context.WithTimeout(ctx, ping)
|
||||
|
||||
// Block on receipt of msg.
|
||||
msg, ok := stream.Recv(pingctx)
|
||||
// Block and wait for
|
||||
// one of the following:
|
||||
//
|
||||
// - receipt of msg
|
||||
// - timeout of pingCtx
|
||||
// - stream closed.
|
||||
msg, haveMsg := stream.Recv(pingCtx)
|
||||
|
||||
// Check if cancel because ping.
|
||||
pinged := (pingctx.Err() != nil)
|
||||
// If ping context has timed
|
||||
// out, we should send a ping.
|
||||
//
|
||||
// In any case cancel pingCtx
|
||||
// as we're done with it.
|
||||
shouldPing := (pingCtx.Err() != nil)
|
||||
cncl()
|
||||
|
||||
switch {
|
||||
case !ok && pinged:
|
||||
// The ping context timed out!
|
||||
l.Trace("writing websocket ping")
|
||||
case !haveMsg && !shouldPing:
|
||||
// We have no message and we shouldn't
|
||||
// send a ping; this means the stream
|
||||
// has been closed from the client's end,
|
||||
// so there's nothing further to do here.
|
||||
l.Trace("no message and we shouldn't ping, returning...")
|
||||
return
|
||||
|
||||
// Wrapped context time-out, send a keep-alive "ping".
|
||||
if err := wsConn.WriteControl(websocket.PingMessage, nil, time.Time{}); err != nil {
|
||||
l.Debugf("error writing websocket ping: %v", err)
|
||||
break
|
||||
case haveMsg:
|
||||
// We have a message to stream.
|
||||
l.Tracef("writing websocket message: %+v", msg)
|
||||
|
||||
if err := wsConn.WriteJSON(msg); err != nil {
|
||||
// If there's an error writing then drop the
|
||||
// connection, as client may have disappeared
|
||||
// suddenly; they can reconnect if necessary.
|
||||
l.Debugf("error writing websocket message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
case !ok:
|
||||
// Stream was
|
||||
// closed.
|
||||
return
|
||||
}
|
||||
case shouldPing:
|
||||
// We have no message but we do
|
||||
// need to send a keep-alive ping.
|
||||
l.Trace("writing websocket ping")
|
||||
|
||||
l.Trace("writing websocket message: %+v", msg)
|
||||
|
||||
// Received a new message from the processor.
|
||||
if err := wsConn.WriteJSON(msg); err != nil {
|
||||
l.Debugf("error writing websocket message: %v", err)
|
||||
break
|
||||
if err := wsConn.WriteControl(websocket.PingMessage, pingMsg, time.Time{}); err != nil {
|
||||
// If there's an error writing then drop the
|
||||
// connection, as client may have disappeared
|
||||
// suddenly; they can reconnect if necessary.
|
||||
l.Debugf("error writing websocket ping: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("finished websocket write")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/streaming"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -92,6 +93,7 @@ func (suite *StreamingTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tags"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
|
|
@ -87,6 +88,7 @@ func (suite *TagsTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
|
|
@ -72,6 +73,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package fileserver_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -98,6 +99,7 @@ func (suite *FileserverTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ type Account struct {
|
|||
// Description of this account's avatar, for alt text.
|
||||
// example: A cute drawing of a smiling sloth.
|
||||
AvatarDescription string `json:"avatar_description,omitempty"`
|
||||
// Database ID of the media attachment for this account's avatar image.
|
||||
// Omitted if no avatar uploaded for this account (ie., default avatar).
|
||||
// example: 01JAJ3XCD66K3T99JZESCR137W
|
||||
AvatarMediaID string `json:"avatar_media_id,omitempty"`
|
||||
// Web location of the account's header image.
|
||||
// example: https://example.org/media/some_user/header/original/header.jpeg
|
||||
Header string `json:"header"`
|
||||
|
|
@ -78,14 +82,18 @@ type Account struct {
|
|||
// Description of this account's header, for alt text.
|
||||
// example: A sunlit field with purple flowers.
|
||||
HeaderDescription string `json:"header_description,omitempty"`
|
||||
// Database ID of the media attachment for this account's header image.
|
||||
// Omitted if no header uploaded for this account (ie., default header).
|
||||
// example: 01JAJ3XCD66K3T99JZESCR137W
|
||||
HeaderMediaID string `json:"header_media_id,omitempty"`
|
||||
// Number of accounts following this account, according to our instance.
|
||||
FollowersCount int `json:"followers_count"`
|
||||
// Number of account's followed by this account, according to our instance.
|
||||
FollowingCount int `json:"following_count"`
|
||||
// Number of statuses posted by this account, according to our instance.
|
||||
StatusesCount int `json:"statuses_count"`
|
||||
// When the account's most recent status was posted (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// When the account's most recent status was posted (ISO 8601 Date).
|
||||
// example: 2021-07-30
|
||||
LastStatusAt *string `json:"last_status_at"`
|
||||
// Array of custom emojis used in this account's note or display name.
|
||||
// Empty for blocked accounts.
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ import "mime/multipart"
|
|||
//
|
||||
// swagger: ignore
|
||||
type AttachmentRequest struct {
|
||||
|
||||
// Media file.
|
||||
File *multipart.FileHeader `form:"file" binding:"required"`
|
||||
|
||||
// Description of the media file. Optional.
|
||||
// This will be used as alt-text for users of screenreaders etc.
|
||||
// example: This is an image of some kittens, they are very cute and fluffy.
|
||||
Description string `form:"description"`
|
||||
|
||||
// Focus of the media file. Optional.
|
||||
// If present, it should be in the form of two comma-separated floats between -1 and 1.
|
||||
// example: -0.5,0.565
|
||||
|
|
@ -39,16 +42,38 @@ type AttachmentRequest struct {
|
|||
//
|
||||
// swagger:ignore
|
||||
type AttachmentUpdateRequest struct {
|
||||
|
||||
// Description of the media file.
|
||||
// This will be used as alt-text for users of screenreaders etc.
|
||||
// allowEmptyValue: true
|
||||
Description *string `form:"description" json:"description" xml:"description"`
|
||||
|
||||
// Focus of the media file.
|
||||
// If present, it should be in the form of two comma-separated floats between -1 and 1.
|
||||
// allowEmptyValue: true
|
||||
Focus *string `form:"focus" json:"focus" xml:"focus"`
|
||||
}
|
||||
|
||||
// AttachmentAttributesRequest models an edit request for attachment attributes.
|
||||
//
|
||||
// swagger:ignore
|
||||
type AttachmentAttributesRequest struct {
|
||||
|
||||
// The ID of the attachment.
|
||||
// example: 01FC31DZT1AYWDZ8XTCRWRBYRK
|
||||
ID string `form:"id" json:"id"`
|
||||
|
||||
// Description of the media file.
|
||||
// This will be used as alt-text for users of screenreaders etc.
|
||||
// allowEmptyValue: true
|
||||
Description string `form:"description" json:"description"`
|
||||
|
||||
// Focus of the media file.
|
||||
// If present, it should be in the form of two comma-separated floats between -1 and 1.
|
||||
// allowEmptyValue: true
|
||||
Focus string `form:"focus" json:"focus"`
|
||||
}
|
||||
|
||||
// Attachment models a media attachment.
|
||||
//
|
||||
// swagger:model attachment
|
||||
|
|
@ -160,7 +185,7 @@ type MediaDimensions struct {
|
|||
Duration float32 `json:"duration,omitempty"`
|
||||
// Bitrate of the media in bits per second.
|
||||
// example: 1000000
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
Bitrate uint64 `json:"bitrate,omitempty"`
|
||||
// Size of the media, in the format `[width]x[height]`.
|
||||
// Not set for audio.
|
||||
// example: 1920x1080
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package model
|
|||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
)
|
||||
|
|
@ -30,8 +29,6 @@ type Content struct {
|
|||
ContentType string
|
||||
// ContentLength in bytes
|
||||
ContentLength int64
|
||||
// Time when the content was last updated.
|
||||
ContentUpdated time.Time
|
||||
// Actual content
|
||||
Content io.ReadCloser
|
||||
// Resource URL to forward to if the file can be fetched from the storage directly (e.g signed S3 URL)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ type Conversation struct {
|
|||
// Is the conversation currently marked as unread?
|
||||
Unread bool `json:"unread"`
|
||||
// Participants in the conversation.
|
||||
//
|
||||
// If this is a conversation between no accounts (ie., a self-directed DM),
|
||||
// this will include only the requesting account itself. Otherwise, it will
|
||||
// include every other account in the conversation *except* the requester.
|
||||
Accounts []Account `json:"accounts"`
|
||||
// The last status in the conversation. May be `null`.
|
||||
LastStatus *Status `json:"last_status"`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ type DomainPermission struct {
|
|||
// Time at which the permission entry was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
// Permission type of this entry (block, allow).
|
||||
// Only set for domain permission drafts.
|
||||
PermissionType string `json:"permission_type,omitempty"`
|
||||
}
|
||||
|
||||
// DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
|
||||
|
|
@ -69,22 +72,24 @@ type DomainPermission struct {
|
|||
type DomainPermissionRequest struct {
|
||||
// A list of domains for which this permission request should apply.
|
||||
// Only used if import=true is specified.
|
||||
Domains *multipart.FileHeader `form:"domains" json:"domains" xml:"domains"`
|
||||
Domains *multipart.FileHeader `form:"domains" json:"domains"`
|
||||
// A single domain for which this permission request should apply.
|
||||
// Only used if import=true is NOT specified or if import=false.
|
||||
// example: example.org
|
||||
Domain string `form:"domain" json:"domain" xml:"domain"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
// Obfuscate the domain name when displaying this permission entry publicly.
|
||||
// Ie., instead of 'example.org' show something like 'e**mpl*.or*'.
|
||||
// example: false
|
||||
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
|
||||
Obfuscate bool `form:"obfuscate" json:"obfuscate"`
|
||||
// Private comment for other admins on why this permission entry was created.
|
||||
// example: don't like 'em!!!!
|
||||
PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"`
|
||||
PrivateComment string `form:"private_comment" json:"private_comment"`
|
||||
// Public comment on why this permission entry was created.
|
||||
// Will be visible to requesters at /api/v1/instance/peers if this endpoint is exposed.
|
||||
// example: foss dorks 😫
|
||||
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
|
||||
PublicComment string `form:"public_comment" json:"public_comment"`
|
||||
// Permission type to create (only applies to domain permission drafts, not explicit blocks and allows).
|
||||
PermissionType string `form:"permission_type" json:"permission_type"`
|
||||
}
|
||||
|
||||
// DomainKeysExpireRequest is the form submitted as a POST to /api/v1/admin/domain_keys_expire to expire a domain's public keys.
|
||||
|
|
@ -92,5 +97,103 @@ type DomainPermissionRequest struct {
|
|||
// swagger:parameters domainKeysExpire
|
||||
type DomainKeysExpireRequest struct {
|
||||
// hostname/domain to expire keys for.
|
||||
Domain string `form:"domain" json:"domain" xml:"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"`
|
||||
// Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
|
||||
// example: 100
|
||||
Priority uint8 `json:"priority"`
|
||||
// Title of this subscription, as set by admin who created or updated it.
|
||||
// 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"`
|
||||
// If true, this domain permission subscription will "adopt" domain permissions which already exist on the instance, and which meet the following conditions: 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present in the subscribed list. Such orphaned domain permissions will be given this subscription's subscription ID value.
|
||||
// example: false
|
||||
AdoptOrphans bool `json:"adopt_orphans"`
|
||||
// Time at which the subscription was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at"`
|
||||
// ID of the account that created this subscription.
|
||||
// example: 01FBW21XJA09XYX51KV5JVBW0F
|
||||
// readonly: true
|
||||
CreatedBy string `json:"created_by"`
|
||||
// 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 of the most recent fetch attempt (successful or otherwise) (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// readonly: true
|
||||
FetchedAt string `json:"fetched_at,omitempty"`
|
||||
// Time of the most recent successful fetch (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// readonly: true
|
||||
SuccessfullyFetchedAt string `json:"successfully_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 {
|
||||
// Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
|
||||
// example: 100
|
||||
Priority *int `form:"priority" json:"priority"`
|
||||
// Title of this subscription, as set by admin who created or updated it.
|
||||
// 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"`
|
||||
// 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"`
|
||||
// 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"`
|
||||
// If true, this domain permission subscription will "adopt" domain permissions
|
||||
// which already exist on the instance, and which meet the following conditions:
|
||||
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
|
||||
// in the subscribed list. Such orphaned domain permissions will be given this
|
||||
// subscription's subscription ID value and be managed by this subscription.
|
||||
AdoptOrphans *bool `form:"adopt_orphans" json:"adopt_orphans"`
|
||||
// (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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,5 +95,5 @@ type FilterCreateUpdateRequestV1 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ type FilterCreateRequestV2 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
|
||||
// Keywords to be added to the newly created filter.
|
||||
Keywords []FilterKeywordCreateUpdateRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
|
||||
|
|
@ -199,7 +199,7 @@ type FilterUpdateRequestV2 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
|
||||
// Keywords to be added to the filter, modified, or removed.
|
||||
Keywords []FilterKeywordCreateUpdateDeleteRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ type InstanceSettingsUpdateRequest struct {
|
|||
ShortDescription *string `form:"short_description" json:"short_description" xml:"short_description"`
|
||||
// Longer description of the instance, max 5,000 chars. HTML formatting accepted.
|
||||
Description *string `form:"description" json:"description" xml:"description"`
|
||||
// Custom CSS for the instance.
|
||||
CustomCSS *string `form:"custom_css" json:"custom_css,omitempty" xml:"custom_css"`
|
||||
// Terms and conditions of the instance, max 5,000 chars. HTML formatting accepted.
|
||||
Terms *string `form:"terms" json:"terms" xml:"terms"`
|
||||
// Image to use as the instance thumbnail.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ type InstanceV1 struct {
|
|||
//
|
||||
// This should be displayed on the 'about' page for an instance.
|
||||
Description string `json:"description"`
|
||||
// Custom CSS for the instance.
|
||||
CustomCSS string `json:"custom_css,omitempty"`
|
||||
// Raw (unparsed) version of description.
|
||||
DescriptionText string `json:"description_text,omitempty"`
|
||||
// A shorter description of the instance.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ type InstanceV2 struct {
|
|||
Description string `json:"description"`
|
||||
// Raw (unparsed) version of description.
|
||||
DescriptionText string `json:"description_text,omitempty"`
|
||||
// Instance Custom Css
|
||||
CustomCSS string `json:"custom_css,omitempty"`
|
||||
// Basic anonymous usage data for this instance.
|
||||
Usage InstanceV2Usage `json:"usage"`
|
||||
// An image used to represent this instance.
|
||||
|
|
|
|||
107
internal/api/model/nullable.go
Normal file
107
internal/api/model/nullable.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Nullable is a generic type, which implements a field that can be one of three states:
|
||||
//
|
||||
// - field is not set in the request
|
||||
// - field is explicitly set to `null` in the request
|
||||
// - field is explicitly set to a valid value in the request
|
||||
//
|
||||
// Nullable is intended to be used with JSON unmarshalling.
|
||||
//
|
||||
// Adapted from https://github.com/oapi-codegen/nullable/blob/main/nullable.go
|
||||
type Nullable[T any] struct {
|
||||
state nullableState
|
||||
value T
|
||||
}
|
||||
|
||||
type nullableState uint8
|
||||
|
||||
const (
|
||||
nullableStateUnspecified nullableState = 0
|
||||
nullableStateNull nullableState = 1
|
||||
nullableStateSet nullableState = 2
|
||||
)
|
||||
|
||||
// Get retrieves the underlying value, if present,
|
||||
// and returns an error if the value was not present.
|
||||
func (t Nullable[T]) Get() (T, error) {
|
||||
var empty T
|
||||
if t.IsNull() {
|
||||
return empty, errors.New("value is null")
|
||||
}
|
||||
|
||||
if !t.IsSpecified() {
|
||||
return empty, errors.New("value is not specified")
|
||||
}
|
||||
|
||||
return t.value, nil
|
||||
}
|
||||
|
||||
// IsNull indicates whether the field
|
||||
// was sent, and had a value of `null`
|
||||
func (t Nullable[T]) IsNull() bool {
|
||||
return t.state == nullableStateNull
|
||||
}
|
||||
|
||||
// IsSpecified indicates whether the field
|
||||
// was sent either as a value or as `null`.
|
||||
func (t Nullable[T]) IsSpecified() bool {
|
||||
return t.state != nullableStateUnspecified
|
||||
}
|
||||
|
||||
// If field is unspecified,
|
||||
// UnmarshalJSON won't be called.
|
||||
func (t *Nullable[T]) UnmarshalJSON(data []byte) error {
|
||||
// If field is specified as `null`.
|
||||
if bytes.Equal(data, []byte("null")) {
|
||||
t.setNull()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, we have an
|
||||
// actual value, so parse it.
|
||||
var v T
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.set(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNull indicates that the field
|
||||
// was sent, and had a value of `null`
|
||||
func (t *Nullable[T]) setNull() {
|
||||
*t = Nullable[T]{state: nullableStateNull}
|
||||
}
|
||||
|
||||
// set the underlying value to given value.
|
||||
func (t *Nullable[T]) set(value T) {
|
||||
*t = Nullable[T]{
|
||||
state: nullableStateSet,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,10 @@ type Status struct {
|
|||
// The date when this status was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at"`
|
||||
// Timestamp of when the status was last edited (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// nullable: true
|
||||
EditedAt *string `json:"edited_at"`
|
||||
// ID of the status being replied to.
|
||||
// example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||
// nullable: true
|
||||
|
|
@ -197,36 +201,50 @@ type StatusReblogged struct {
|
|||
//
|
||||
// swagger:ignore
|
||||
type StatusCreateRequest struct {
|
||||
|
||||
// Text content of the status.
|
||||
// If media_ids is provided, this becomes optional.
|
||||
// Attaching a poll is optional while status is provided.
|
||||
Status string `form:"status" json:"status"`
|
||||
|
||||
// Array of Attachment ids to be attached as media.
|
||||
// If provided, status becomes optional, and poll cannot be used.
|
||||
MediaIDs []string `form:"media_ids[]" json:"media_ids"`
|
||||
|
||||
// Poll to include with this status.
|
||||
Poll *PollRequest `form:"poll" json:"poll"`
|
||||
|
||||
// ID of the status being replied to, if status is a reply.
|
||||
InReplyToID string `form:"in_reply_to_id" json:"in_reply_to_id"`
|
||||
|
||||
// Status and attached media should be marked as sensitive.
|
||||
Sensitive bool `form:"sensitive" json:"sensitive"`
|
||||
|
||||
// Text to be shown as a warning or subject before the actual content.
|
||||
// Statuses are generally collapsed behind this field.
|
||||
SpoilerText string `form:"spoiler_text" json:"spoiler_text"`
|
||||
|
||||
// Visibility of the posted status.
|
||||
Visibility Visibility `form:"visibility" json:"visibility"`
|
||||
// Set to "true" if this status should not be federated, ie. it should be a "local only" status.
|
||||
|
||||
// Set to "true" if this status should not be
|
||||
// federated,ie. it should be a "local only" status.
|
||||
LocalOnly *bool `form:"local_only" json:"local_only"`
|
||||
|
||||
// Deprecated: Only used if LocalOnly is not set.
|
||||
Federated *bool `form:"federated" json:"federated"`
|
||||
|
||||
// ISO 8601 Datetime at which to schedule a status.
|
||||
// Providing this parameter will cause ScheduledStatus to be returned instead of Status.
|
||||
// Must be at least 5 minutes in the future.
|
||||
ScheduledAt string `form:"scheduled_at" json:"scheduled_at"`
|
||||
|
||||
// ISO 639 language code for this status.
|
||||
Language string `form:"language" json:"language"`
|
||||
|
||||
// Content type to use when parsing this status.
|
||||
ContentType StatusContentType `form:"content_type" json:"content_type"`
|
||||
|
||||
// Interaction policy to use for this status.
|
||||
InteractionPolicy *InteractionPolicy `form:"-" json:"interaction_policy"`
|
||||
}
|
||||
|
|
@ -236,6 +254,7 @@ type StatusCreateRequest struct {
|
|||
//
|
||||
// swagger:ignore
|
||||
type StatusInteractionPolicyForm struct {
|
||||
|
||||
// Interaction policy to use for this status.
|
||||
InteractionPolicy *InteractionPolicy `form:"interaction_policy" json:"-"`
|
||||
}
|
||||
|
|
@ -250,13 +269,18 @@ const (
|
|||
// VisibilityNone is visible to nobody. This is only used for the visibility of web statuses.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic is visible to everyone, and will be available via the web even for nonauthenticated users.
|
||||
|
||||
VisibilityPublic Visibility = "public"
|
||||
|
||||
// VisibilityUnlisted is visible to everyone, but only on home timelines, lists, etc.
|
||||
VisibilityUnlisted Visibility = "unlisted"
|
||||
|
||||
// VisibilityPrivate is visible only to followers of the account that posted the status.
|
||||
VisibilityPrivate Visibility = "private"
|
||||
|
||||
// VisibilityMutualsOnly is visible only to mutual followers of the account that posted the status.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
|
||||
// VisibilityDirect is visible only to accounts tagged in the status. It is equivalent to a direct message.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
)
|
||||
|
|
@ -268,7 +292,8 @@ const (
|
|||
// swagger:type string
|
||||
type StatusContentType string
|
||||
|
||||
// Content type to use when parsing submitted status into an html-formatted status
|
||||
// Content type to use when parsing submitted
|
||||
// status into an html-formatted status.
|
||||
const (
|
||||
StatusContentTypePlain StatusContentType = "text/plain"
|
||||
StatusContentTypeMarkdown StatusContentType = "text/markdown"
|
||||
|
|
@ -280,11 +305,14 @@ const (
|
|||
//
|
||||
// swagger:model statusSource
|
||||
type StatusSource struct {
|
||||
|
||||
// ID of the status.
|
||||
// example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||
ID string `json:"id"`
|
||||
|
||||
// Plain-text source of a status.
|
||||
Text string `json:"text"`
|
||||
|
||||
// Plain-text version of spoiler text.
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
}
|
||||
|
|
@ -294,27 +322,69 @@ type StatusSource struct {
|
|||
//
|
||||
// swagger:model statusEdit
|
||||
type StatusEdit struct {
|
||||
|
||||
// The content of this status at this revision.
|
||||
// Should be HTML, but might also be plaintext in some cases.
|
||||
// example: <p>Hey this is a status!</p>
|
||||
Content string `json:"content"`
|
||||
|
||||
// Subject, summary, or content warning for the status at this revision.
|
||||
// example: warning nsfw
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
|
||||
// Status marked sensitive at this revision.
|
||||
// example: false
|
||||
Sensitive bool `json:"sensitive"`
|
||||
|
||||
// The date when this revision was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at"`
|
||||
|
||||
// The account that authored this status.
|
||||
Account *Account `json:"account"`
|
||||
|
||||
// The poll attached to the status at this revision.
|
||||
// Note that edits changing the poll options will be collapsed together into one edit, since this action resets the poll.
|
||||
// nullable: true
|
||||
Poll *Poll `json:"poll"`
|
||||
|
||||
// Media that is attached to this status.
|
||||
MediaAttachments []*Attachment `json:"media_attachments"`
|
||||
|
||||
// Custom emoji to be used when rendering status content.
|
||||
Emojis []Emoji `json:"emojis"`
|
||||
}
|
||||
|
||||
// StatusEditRequest models status edit parameters.
|
||||
//
|
||||
// swagger:ignore
|
||||
type StatusEditRequest struct {
|
||||
|
||||
// Text content of the status.
|
||||
// If media_ids is provided, this becomes optional.
|
||||
// Attaching a poll is optional while status is provided.
|
||||
Status string `form:"status" json:"status"`
|
||||
|
||||
// Text to be shown as a warning or subject before the actual content.
|
||||
// Statuses are generally collapsed behind this field.
|
||||
SpoilerText string `form:"spoiler_text" json:"spoiler_text"`
|
||||
|
||||
// Content type to use when parsing this status.
|
||||
ContentType StatusContentType `form:"content_type" json:"content_type"`
|
||||
|
||||
// Status and attached media should be marked as sensitive.
|
||||
Sensitive bool `form:"sensitive" json:"sensitive"`
|
||||
|
||||
// ISO 639 language code for this status.
|
||||
Language string `form:"language" json:"language"`
|
||||
|
||||
// Array of Attachment ids to be attached as media.
|
||||
// If provided, status becomes optional, and poll cannot be used.
|
||||
MediaIDs []string `form:"media_ids[]" json:"media_ids"`
|
||||
|
||||
// Array of Attachment attributes to be updated in attached media.
|
||||
MediaAttributes []AttachmentAttributesRequest `form:"media_attributes[]" json:"media_attributes"`
|
||||
|
||||
// Poll to include with this status.
|
||||
Poll *PollRequest `form:"poll" json:"poll"`
|
||||
}
|
||||
|
|
|
|||
130
internal/api/util/parseform.go
Normal file
130
internal/api/util/parseform.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// 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 util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// ParseFocus parses a media attachment focus parameters from incoming API string.
|
||||
func ParseFocus(focus string) (focusx, focusy float32, errWithCode gtserror.WithCode) {
|
||||
if focus == "" {
|
||||
return
|
||||
}
|
||||
spl := strings.Split(focus, ",")
|
||||
if len(spl) != 2 {
|
||||
const text = "missing comma separator"
|
||||
errWithCode = gtserror.NewErrorBadRequest(
|
||||
errors.New(text),
|
||||
text,
|
||||
)
|
||||
return
|
||||
}
|
||||
xStr := spl[0]
|
||||
yStr := spl[1]
|
||||
fx, err := strconv.ParseFloat(xStr, 32)
|
||||
if err != nil || fx > 1 || fx < -1 {
|
||||
text := fmt.Sprintf("invalid x focus: %s", xStr)
|
||||
errWithCode = gtserror.NewErrorBadRequest(
|
||||
errors.New(text),
|
||||
text,
|
||||
)
|
||||
return
|
||||
}
|
||||
fy, err := strconv.ParseFloat(yStr, 32)
|
||||
if err != nil || fy > 1 || fy < -1 {
|
||||
text := fmt.Sprintf("invalid y focus: %s", xStr)
|
||||
errWithCode = gtserror.NewErrorBadRequest(
|
||||
errors.New(text),
|
||||
text,
|
||||
)
|
||||
return
|
||||
}
|
||||
focusx = float32(fx)
|
||||
focusy = float32(fy)
|
||||
return
|
||||
}
|
||||
|
||||
// ParseDuration parses the given raw interface belonging
|
||||
// the given fieldName as an integer duration.
|
||||
func ParseDuration(rawI any, fieldName string) (*int, error) {
|
||||
var (
|
||||
asInteger int
|
||||
err error
|
||||
)
|
||||
|
||||
switch raw := rawI.(type) {
|
||||
case float64:
|
||||
// Submitted as JSON number
|
||||
// (casts to float64 by default).
|
||||
asInteger = int(raw)
|
||||
|
||||
case string:
|
||||
// Submitted as JSON string or form field.
|
||||
asInteger, err = strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(
|
||||
"could not parse %s value %s as integer: %w",
|
||||
fieldName, raw, err,
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
// Submitted as god-knows-what.
|
||||
err = fmt.Errorf(
|
||||
"could not parse %s type %T as integer",
|
||||
fieldName, rawI,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &asInteger, nil
|
||||
}
|
||||
|
||||
// ParseNullableDuration is like ParseDuration, but
|
||||
// for JSON values that may have been sent as `null`.
|
||||
//
|
||||
// IsSpecified should be checked and "true" on the
|
||||
// given nullable before calling this function.
|
||||
func ParseNullableDuration(
|
||||
nullable apimodel.Nullable[any],
|
||||
fieldName string,
|
||||
) (*int, error) {
|
||||
if nullable.IsNull() {
|
||||
// Was specified as `null`,
|
||||
// return pointer to zero value.
|
||||
return util.Ptr(0), nil
|
||||
}
|
||||
|
||||
rawI, err := nullable.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseDuration(rawI, fieldName)
|
||||
}
|
||||
|
|
@ -69,8 +69,11 @@ const (
|
|||
|
||||
/* Domain permission keys */
|
||||
|
||||
DomainPermissionExportKey = "export"
|
||||
DomainPermissionImportKey = "import"
|
||||
DomainPermissionExportKey = "export"
|
||||
DomainPermissionImportKey = "import"
|
||||
DomainPermissionSubscriptionIDKey = "subscription_id"
|
||||
DomainPermissionPermTypeKey = "permission_type"
|
||||
DomainPermissionDomainKey = "domain"
|
||||
|
||||
/* Admin query keys */
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package webfinger_test
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
|
|
@ -79,6 +80,7 @@ func (suite *WebfingerStandardTestSuite) SetupTest() {
|
|||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
suite.tc = typeutils.NewConverter(&suite.state)
|
||||
|
||||
testrig.StartTimelines(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue