Merge branch 'main' into instance-custom-css

This commit is contained in:
tobi 2024-12-02 10:55:05 +01:00
commit 7cd9b0eae0
947 changed files with 689490 additions and 178161 deletions

View file

@ -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"
)
@ -99,7 +98,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
suite.Equal(2, apimodelAccount.FollowersCount)
suite.Equal(2, apimodelAccount.FollowingCount)
suite.Equal(8, apimodelAccount.StatusesCount)
suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy)
suite.EqualValues(apimodel.VisibilityPublic, apimodelAccount.Source.Privacy)
suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language)
suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)
}

View file

@ -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.

View file

@ -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() {

View file

@ -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",
"last_status_at": "2021-07-28",
"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",
"last_status_at": "2024-01-10",
"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",
"last_status_at": "2021-09-11",
"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,

View file

@ -28,37 +28,43 @@ 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
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 +105,19 @@ 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)
// header filtering administration routes
attachHandler(http.MethodGet, HeaderAllowsPathWithID, m.HeaderFilterAllowGET)
attachHandler(http.MethodGet, HeaderBlocksPathWithID, m.HeaderFilterBlockGET)

View 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)
}

View file

@ -0,0 +1,176 @@
// 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/gtsmodel"
"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
}
var (
permType gtsmodel.DomainPermissionType
errText string
)
switch pt := form.PermissionType; pt {
case "block":
permType = gtsmodel.DomainPermissionBlock
case "allow":
permType = gtsmodel.DomainPermissionAllow
case "":
errText = "permission_type not set, must be one of block or allow"
default:
errText = fmt.Sprintf("permission_type %s not recognized, must be one of block or allow", pt)
}
if errText != "" {
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
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)
}

View 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)
}

View 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)
}

View file

@ -0,0 +1,185 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package 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 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

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

View file

@ -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",
"last_status_at": "2021-09-11",
"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",
"last_status_at": "2021-07-28",
"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",
"last_status_at": "2021-07-28",
"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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
}
@ -512,10 +518,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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
},
@ -657,10 +664,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",
"last_status_at": "2021-07-28",
"emojis": [],
"fields": [
{
@ -715,10 +723,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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
}
@ -762,10 +771,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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
},
@ -907,10 +917,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",
"last_status_at": "2021-07-28",
"emojis": [],
"fields": [
{
@ -965,10 +976,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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
}
@ -1012,10 +1024,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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
},

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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": []
}

View file

@ -80,7 +80,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>",
@ -120,7 +120,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 +130,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 +155,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -174,10 +174,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 +221,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>",
@ -260,7 +261,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 +271,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 +296,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -314,10 +315,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 +362,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>",
@ -400,7 +402,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 +412,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 +437,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -454,10 +456,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 +554,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>",
@ -591,7 +594,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 +604,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 +629,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -645,10 +648,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 +717,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>",
@ -753,7 +757,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 +767,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 +792,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
@ -811,10 +815,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 +863,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 +899,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>",
@ -934,7 +939,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 +949,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 +974,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
},
"stats": {
"domain_count": 2,
"status_count": 20,
"status_count": 19,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
@ -988,10 +993,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,

View file

@ -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":3,"last_status_at":"2021-09-11","emojis":[],"fields":[],"mute_expires_at":null}]`)
if err != nil {
suite.FailNow(err.Error())
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
}

View file

@ -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",
"last_status_at": "2021-09-11",
"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",
"last_status_at": "2021-09-11",
"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",
"last_status_at": "2021-09-11",
"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",
"last_status_at": "2021-09-11",
"emojis": [],
"fields": []
}

View file

@ -591,7 +591,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 +601,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

View file

@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@ -181,7 +180,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 +253,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 {
@ -286,8 +287,8 @@ 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)
if errWithCode := validateStatusCreateForm(form); errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
@ -374,46 +375,61 @@ func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error
return form, nil
}
// validateNormalizeCreateStatus checks the form
// for disallowed combinations of attachments and
// overlength inputs.
// validateStatusCreateForm checks the form for disallowed
// combinations of attachments, overlength inputs, etc.
//
// 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
func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithCode {
var (
chars = len([]rune(form.Status)) + len([]rune(form.SpoilerText))
maxChars = config.GetStatusesMaxChars()
mediaFiles = len(form.MediaIDs)
maxMediaFiles = config.GetStatusesMediaMaxFiles()
hasMedia = mediaFiles != 0
hasPoll = form.Poll != nil
)
if !hasStatus && !hasMedia && !hasPoll {
return errors.New("no status, media, or poll provided")
if chars == 0 && !hasMedia && !hasPoll {
// Status must contain *some* kind of content.
const text = "no status content, content warning, media, or poll provided"
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if hasMedia && hasPoll {
return errors.New("can't post media + poll in same status")
if chars > maxChars {
text := fmt.Sprintf(
"status too long, %d characters provided (including content warning) but limit is %d",
chars, maxChars,
)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
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 mediaFiles > maxMediaFiles {
text := fmt.Sprintf(
"too many media files attached to status, %d attached but limit is %d",
mediaFiles, maxMediaFiles,
)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if form.Poll != nil {
if err := validateNormalizeCreatePoll(form); err != nil {
return err
if errWithCode := validateStatusPoll(form); errWithCode != nil {
return errWithCode
}
}
if form.ScheduledAt != "" {
const text = "scheduled_at is not yet implemented"
return gtserror.NewErrorNotImplemented(errors.New(text), text)
}
// Validate + normalize
// language tag if provided.
if form.Language != "" {
language, err := validate.Language(form.Language)
lang, err := validate.Language(form.Language)
if err != nil {
return err
return gtserror.NewErrorBadRequest(err, err.Error())
}
form.Language = language
form.Language = lang
}
// Check if the deprecated "federated" field was
@ -425,42 +441,51 @@ func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error {
return nil
}
func validateNormalizeCreatePoll(form *apimodel.StatusCreateRequest) error {
maxPollOptions := config.GetStatusesPollMaxOptions()
maxPollChars := config.GetStatusesPollOptionMaxChars()
func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode {
var (
maxPollOptions = config.GetStatusesPollMaxOptions()
pollOptions = len(form.Poll.Options)
maxPollOptionChars = 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)
if pollOptions == 0 {
const text = "poll with no options"
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
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)
}
if pollOptions > maxPollOptions {
text := fmt.Sprintf(
"too many poll options provided, %d provided but limit is %d",
pollOptions, maxPollOptions,
)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
form.Poll.ExpiresIn = expiresIn
default:
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
for _, option := range form.Poll.Options {
optionChars := len([]rune(option))
if optionChars > maxPollOptionChars {
text := fmt.Sprintf(
"poll option too long, %d characters provided but limit is %d",
optionChars, maxPollOptionChars,
)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
}
if len(form.Poll.Options) == 0 {
return errors.New("poll with no options")
}
// Normalize poll expiry if necessary.
if 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 gtserror.NewErrorBadRequest(err, err.Error())
}
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)
if expiresIn != nil {
form.Poll.ExpiresIn = *expiresIn
}
}

View file

@ -365,6 +365,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},

View file

@ -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"
@ -185,13 +186,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,
@ -268,30 +280,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) {

View file

@ -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",
"last_status_at": "2024-01-10",
"emojis": [],
"fields": [],
"enable_rss": true

View file

@ -127,13 +127,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",
"last_status_at": "2024-01-10",
"emojis": [],
"fields": [],
"enable_rss": true
@ -212,13 +214,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",
"last_status_at": "2024-01-10",
"emojis": [],
"fields": [],
"enable_rss": true

View file

@ -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")
}