mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2026-01-04 09:13:16 -06:00
Merge branch 'main' into instance-custom-css
This commit is contained in:
commit
7cd9b0eae0
947 changed files with 689490 additions and 178161 deletions
|
|
@ -77,6 +77,10 @@ const (
|
|||
// See https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes
|
||||
// and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
TagHashtag = "Hashtag"
|
||||
|
||||
// Not in the AS spec, just used internally to indicate
|
||||
// that we don't *yet* know what type of Object something is.
|
||||
ObjectUnknown = "Unknown"
|
||||
)
|
||||
|
||||
// isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity).
|
||||
|
|
|
|||
|
|
@ -1027,7 +1027,7 @@ func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmo
|
|||
)
|
||||
|
||||
if len(to) == 0 && len(cc) == 0 {
|
||||
return "", gtserror.Newf("message wasn't TO or CC anyone")
|
||||
return 0, gtserror.Newf("message wasn't TO or CC anyone")
|
||||
}
|
||||
|
||||
// Assume most restrictive visibility,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ package accounts
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -140,25 +138,15 @@ func normalizeCreateUpdateMute(form *apimodel.UserMuteCreateUpdateRequest) error
|
|||
// Apply defaults for missing fields.
|
||||
form.Notifications = util.Ptr(util.PtrOrValue(form.Notifications, false))
|
||||
|
||||
// Normalize mute duration if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.DurationI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.Duration = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
duration, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse duration value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.Duration = &duration
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// Normalize duration if necessary.
|
||||
if form.DurationI != nil {
|
||||
// If we parsed this as JSON, duration
|
||||
// may be either a float64 or a string.
|
||||
duration, err := apiutil.ParseDuration(form.DurationI, "duration")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Duration = duration
|
||||
}
|
||||
|
||||
// Interpret zero as indefinite duration.
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
|
|||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||
}
|
||||
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01J5QVB9VC76NPPRQ207GG4DRZ&exclude_replies=false&exclude_reblogs=false&pinned=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
}
|
||||
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
|
||||
|
|
|
|||
|
|
@ -96,10 +96,11 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
134
internal/api/client/admin/domainpermissiondraftaccept.go
Normal file
134
internal/api/client/admin/domainpermissiondraftaccept.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftAcceptPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/accept domainPermissionDraftAccept
|
||||
//
|
||||
// Accept a domain permission draft, turning it into an enforced domain permission.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
// -
|
||||
// name: overwrite
|
||||
// in: formData
|
||||
// description: >-
|
||||
// If a domain permission already exists with the same
|
||||
// domain and permission type as the draft, overwrite
|
||||
// the existing permission with fields from the draft.
|
||||
// type: boolean
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The newly created domain permission.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionDraftAcceptPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
type AcceptForm struct {
|
||||
Overwrite bool `json:"overwrite" form:"overwrite"`
|
||||
}
|
||||
|
||||
form := new(AcceptForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, _, errWithCode := m.processor.Admin().DomainPermissionDraftAccept(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
form.Overwrite,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
176
internal/api/client/admin/domainpermissiondraftcreate.go
Normal file
176
internal/api/client/admin/domainpermissiondraftcreate.go
Normal 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)
|
||||
}
|
||||
104
internal/api/client/admin/domainpermissiondraftget.go
Normal file
104
internal/api/client/admin/domainpermissiondraftget.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts/{id} domainPermissionDraftGet
|
||||
//
|
||||
// Get domain permission draft with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission draft.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionDraftGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permDraft, errWithCode := m.processor.Admin().DomainPermissionDraftGet(c.Request.Context(), id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permDraft)
|
||||
}
|
||||
134
internal/api/client/admin/domainpermissiondraftremove.go
Normal file
134
internal/api/client/admin/domainpermissiondraftremove.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/remove domainPermissionDraftRemove
|
||||
//
|
||||
// Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission draft.
|
||||
// type: string
|
||||
// -
|
||||
// name: exclude_target
|
||||
// in: formData
|
||||
// description: >-
|
||||
// When removing the domain permission draft, also create a
|
||||
// domain exclude entry for the target domain, so that drafts
|
||||
// will not be created for this domain in the future.
|
||||
// type: boolean
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The removed domain permission draft.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionDraftRemovePOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
type RemoveForm struct {
|
||||
ExcludeTarget bool `json:"exclude_target" form:"exclude_target"`
|
||||
}
|
||||
|
||||
form := new(RemoveForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, errWithCode := m.processor.Admin().DomainPermissionDraftRemove(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
form.ExcludeTarget,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
185
internal/api/client/admin/domainpermissiondraftsget.go
Normal file
185
internal/api/client/admin/domainpermissiondraftsget.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftsGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts domainPermissionDraftsGet
|
||||
//
|
||||
// View domain permission drafts.
|
||||
//
|
||||
// The drafts will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
|
||||
//
|
||||
// The next and previous queries can be parsed from the returned Link header.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: subscription_id
|
||||
// type: string
|
||||
// description: Show only drafts created by the given subscription ID.
|
||||
// in: query
|
||||
// -
|
||||
// name: domain
|
||||
// type: string
|
||||
// description: Return only drafts that target the given domain.
|
||||
// in: query
|
||||
// -
|
||||
// name: permission_type
|
||||
// type: string
|
||||
// description: Filter on "block" or "allow" type drafts.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *OLDER* than the given max ID (for paging downwards).
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: since_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *NEWER* than the given since ID.
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: min_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items immediately *NEWER* than the given min ID (for paging upwards).
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// minimum: 1
|
||||
// maximum: 100
|
||||
// in: query
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission drafts.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionDraftsGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permTypeStr := c.Query(apiutil.DomainPermissionPermTypeKey)
|
||||
permType := gtsmodel.ParseDomainPermissionType(permTypeStr)
|
||||
if permType == gtsmodel.DomainPermissionUnknown {
|
||||
text := fmt.Sprintf(
|
||||
"permission_type %s not recognized, valid values are empty string, block, or allow",
|
||||
permTypeStr,
|
||||
)
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionDraftsGet(
|
||||
c.Request.Context(),
|
||||
c.Query(apiutil.DomainPermissionSubscriptionIDKey),
|
||||
c.Query(apiutil.DomainPermissionDomainKey),
|
||||
permType,
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
138
internal/api/client/admin/domainpermissionexcludecreate.go
Normal file
138
internal/api/client/admin/domainpermissionexcludecreate.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludesPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_excludes domainPermissionExcludeCreate
|
||||
//
|
||||
// Create a domain permission exclude with the given parameters.
|
||||
//
|
||||
// Excluded domains (and their subdomains) will not be automatically blocked or allowed when a list of domain permissions is imported or subscribed to.
|
||||
//
|
||||
// You can still manually create domain blocks or domain allows for excluded domains, and any new or existing domain blocks or domain allows for an excluded domain will still be enforced.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/json
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: domain
|
||||
// in: formData
|
||||
// description: Domain to create the permission exclude for.
|
||||
// type: string
|
||||
// -
|
||||
// name: private_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Private comment about this domain exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The newly created domain permission exclude.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionExcludesPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse + validate form.
|
||||
type ExcludeForm struct {
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
PrivateComment string `form:"private_comment" json:"private_comment"`
|
||||
}
|
||||
|
||||
form := new(ExcludeForm)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Domain == "" {
|
||||
const errText = "domain must be set"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permExclude, errWithCode := m.processor.Admin().DomainPermissionExcludeCreate(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
form.Domain,
|
||||
form.PrivateComment,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permExclude)
|
||||
}
|
||||
104
internal/api/client/admin/domainpermissionexcludeget.go
Normal file
104
internal/api/client/admin/domainpermissionexcludeget.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludeGETHandler swagger:operation GET /api/v1/admin/domain_permission_excludes/{id} domainPermissionExcludeGet
|
||||
//
|
||||
// Get domain permission exclude with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission exclude.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionExcludeGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
permExclude, errWithCode := m.processor.Admin().DomainPermissionExcludeGet(c.Request.Context(), id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, permExclude)
|
||||
}
|
||||
110
internal/api/client/admin/domainpermissionexcluderemove.go
Normal file
110
internal/api/client/admin/domainpermissionexcluderemove.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludeDELETEHandler swagger:operation DELETE /api/v1/admin/domain_permission_excludes/{id} domainPermissionExcludeDelete
|
||||
//
|
||||
// Remove a domain permission exclude.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// required: true
|
||||
// in: path
|
||||
// description: ID of the domain permission exclude.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The removed domain permission exclude.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionExcludeDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainPerm, errWithCode := m.processor.Admin().DomainPermissionExcludeRemove(
|
||||
c.Request.Context(),
|
||||
authed.Account,
|
||||
id,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainPerm)
|
||||
}
|
||||
159
internal/api/client/admin/domainpermissionexcludesget.go
Normal file
159
internal/api/client/admin/domainpermissionexcludesget.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// DomainPermissionExcludesGETHandler swagger:operation GET /api/v1/admin/domain_permission_excludes domainPermissionExcludesGet
|
||||
//
|
||||
// View domain permission excludes.
|
||||
//
|
||||
// The excludes will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
|
||||
//
|
||||
// The next and previous queries can be parsed from the returned Link header.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// <https://example.org/api/v1/admin/domain_permission_excludes?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_excludes?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: domain
|
||||
// type: string
|
||||
// description: Return only excludes that target the given domain.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *OLDER* than the given max ID (for paging downwards).
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: since_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *NEWER* than the given since ID.
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: min_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items immediately *NEWER* than the given min ID (for paging upwards).
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// minimum: 1
|
||||
// maximum: 100
|
||||
// in: query
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Domain permission excludes.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainPermissionExcludesGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Admin().DomainPermissionExcludesGet(
|
||||
c.Request.Context(),
|
||||
c.Query(apiutil.DomainPermissionDomainKey),
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ import (
|
|||
// The code to use for the emoji, which will be used by instance denizens to select it.
|
||||
// This must be unique on the instance.
|
||||
// type: string
|
||||
// pattern: \w{2,30}
|
||||
// pattern: \w{1,30}
|
||||
// required: true
|
||||
// -
|
||||
// name: image
|
||||
|
|
@ -145,8 +145,8 @@ func validateCreateEmoji(form *apimodel.EmojiCreateRequest) error {
|
|||
return errors.New("no emoji given")
|
||||
}
|
||||
|
||||
maxSize := config.GetMediaEmojiLocalMaxSize()
|
||||
if form.Image.Size > int64(maxSize) {
|
||||
maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
|
||||
if form.Image.Size > maxSize {
|
||||
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import (
|
|||
// The code to use for the emoji, which will be used by instance denizens to select it.
|
||||
// This must be unique on the instance. Works for the `copy` action type only.
|
||||
// type: string
|
||||
// pattern: \w{2,30}
|
||||
// pattern: \w{1,30}
|
||||
// -
|
||||
// name: image
|
||||
// in: formData
|
||||
|
|
@ -208,8 +208,8 @@ func validateUpdateEmoji(form *apimodel.EmojiUpdateRequest) error {
|
|||
}
|
||||
|
||||
if hasImage {
|
||||
maxSize := config.GetMediaEmojiLocalMaxSize()
|
||||
if form.Image.Size > int64(maxSize) {
|
||||
maxSize := int64(config.GetMediaEmojiLocalMaxSize()) // #nosec G115 -- Already validated.
|
||||
if form.Image.Size > maxSize {
|
||||
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 2 and 30 characters, letters, numbers, and underscores only"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: shortcode did not pass validation, must be between 1 and 30 characters, letters, numbers, and underscores only"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() {
|
||||
|
|
|
|||
|
|
@ -183,10 +183,11 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"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": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func (suite *FiltersTestSuite) postFilter(
|
|||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
expiresInStr *string,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
|
|
@ -75,6 +76,8 @@ func (suite *FiltersTestSuite) postFilter(
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
|||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -155,7 +158,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
|||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -182,7 +185,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -203,7 +206,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() {
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -211,7 +214,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -220,7 +223,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -228,7 +231,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -237,8 +240,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
|||
// There should be a filter with this phrase as its title in our test fixtures. Creating another should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
phrase := "fnord"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// postFilterWithExpiration creates a filter with optional expiration.
|
||||
func (suite *FiltersTestSuite) postFilterWithExpiration(phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() {
|
||||
title := "Form Sins"
|
||||
expiresInStr := ""
|
||||
filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() {
|
||||
requestJson := `{
|
||||
"phrase": "JSON Sins",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ func (suite *FiltersTestSuite) putFilter(
|
|||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
expiresInStr *string,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
|
|
@ -76,6 +77,8 @@ func (suite *FiltersTestSuite) putFilter(
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +131,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
|||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -160,7 +163,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
|||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -188,7 +191,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -210,7 +213,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -219,7 +222,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterMissingPhrase() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -229,7 +232,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -238,7 +241,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterMissingContext() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -248,7 +251,7 @@ func (suite *FiltersTestSuite) TestPutFilterMissingContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "metasyntactic variables"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -258,7 +261,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
|||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -268,8 +271,60 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
|||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// setFilterExpiration sets filter expiration.
|
||||
func (suite *FiltersTestSuite) setFilterExpiration(id string, phrase *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV1 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, phrase, &context, nil, nil, expiresIn, expiresInStr, requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() {
|
||||
filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
id := filterKeyword.ID
|
||||
phrase := filterKeyword.Keyword
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to an empty string.
|
||||
expiresInStr := ""
|
||||
filter = suite.setFilterExpiration(id, &phrase, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() {
|
||||
filterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
id := filterKeyword.ID
|
||||
phrase := filterKeyword.Keyword
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &phrase, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to a null literal.
|
||||
requestJson := `{
|
||||
"phrase": "fnord",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,14 @@ package v1
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1) error {
|
||||
func validateNormalizeCreateUpdateFilter(form *apimodel.FilterCreateUpdateRequestV1) error {
|
||||
if err := validate.FilterKeyword(form.Phrase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -47,24 +46,16 @@ func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1
|
|||
return errors.New("irreversible aka server-side drop filters are not supported yet")
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -227,24 +225,16 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
|||
// Apply defaults for missing fields.
|
||||
form.FilterAction = util.Ptr(action)
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesKeyword *[]string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
|
|
@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, acti
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
if keywordsAttributesKeyword != nil {
|
||||
ctx.Request.Form["keywords_attributes[][keyword]"] = *keywordsAttributesKeyword
|
||||
|
|
@ -130,7 +132,7 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
|||
keywordsAttributesWholeWord := []bool{true, false}
|
||||
// Checked in lexical order by status ID, so keep this sorted.
|
||||
statusAttributesStatusID := []string{"01HEN2QRFA8H3C6QPN7RD4KSR6", "01HEWV37MHV8BAC8ANFGVRRM5D"}
|
||||
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, nil, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "", &keywordsAttributesKeyword)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -197,7 +199,7 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
|||
}
|
||||
]
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -245,7 +247,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -267,7 +269,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -275,7 +277,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -284,7 +286,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
|||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -292,7 +294,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
|||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
title := "GNU/Linux"
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -301,8 +303,37 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
|||
// Creating another filter with the same title should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
title := suite.testFilters["local_account_1_filter_1"].Title
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// postFilterWithExpiration creates a filter with optional expiration.
|
||||
func (suite *FiltersTestSuite) postFilterWithExpiration(title *string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 {
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(title, &context, nil, expiresIn, expiresInStr, nil, nil, requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithEmptyStringExpiration() {
|
||||
title := "Form Crimes"
|
||||
expiresInStr := ""
|
||||
filter := suite.postFilterWithExpiration(&title, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPostFilterWithNullExpirationJSON() {
|
||||
requestJson := `{
|
||||
"title": "JSON Crimes",
|
||||
"context": ["home"],
|
||||
"expires_in": null
|
||||
}`
|
||||
filter := suite.postFilterWithExpiration(nil, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ package v2
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
|
@ -271,24 +269,16 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
// If `expires_in` was provided
|
||||
// as JSON, then normalize it.
|
||||
if form.ExpiresInI.IsSpecified() {
|
||||
var err error
|
||||
form.ExpiresIn, err = apiutil.ParseNullableDuration(
|
||||
form.ExpiresInI,
|
||||
"expires_in",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesID *[]string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, expiresInStr *string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string, keywordsAttributesID *[]string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
|
|
@ -64,6 +64,8 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context
|
|||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
} else if expiresInStr != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{*expiresInStr}
|
||||
}
|
||||
if keywordsAttributesID != nil {
|
||||
ctx.Request.Form["keywords_attributes[][id]"] = *keywordsAttributesID
|
||||
|
|
@ -159,7 +161,7 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
|||
keywordsAttributesWholeWord := []bool{true, false, true}
|
||||
keywordsAttributesDestroy := []bool{false, true}
|
||||
statusesAttributesStatusID := []string{suite.testStatuses["remote_account_1_status_2"].ID}
|
||||
filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, &keywordsAttributesID, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, nil, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "", &keywordsAttributesID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -231,7 +233,7 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
|||
}
|
||||
]
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -281,7 +283,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
|
||||
filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -302,7 +304,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -312,7 +314,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -322,7 +324,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
|||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := suite.testFilters["local_account_1_filter_2"].Title
|
||||
_, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`)
|
||||
_, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -332,7 +334,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
|||
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
@ -342,8 +344,70 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
|||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// setFilterExpiration sets filter expiration.
|
||||
func (suite *FiltersTestSuite) setFilterExpiration(id string, expiresIn *int, expiresInStr *string, requestJson *string) *apimodel.FilterV2 {
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, expiresIn, expiresInStr, nil, nil, nil, nil, nil, nil, requestJson, http.StatusOK, "", nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateEmptyString() {
|
||||
id := suite.testFilters["local_account_1_filter_2"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to an empty string.
|
||||
expiresInStr := ""
|
||||
filter = suite.setFilterExpiration(id, nil, &expiresInStr, nil)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnsetExpirationDateNullJSON() {
|
||||
id := suite.testFilters["local_account_1_filter_3"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Unset the filter's expiration date by setting it to a null literal.
|
||||
requestJson := `{
|
||||
"expires_in": null
|
||||
}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, &requestJson)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
// Regression test related to https://github.com/superseriousbusiness/gotosocial/issues/3497
|
||||
func (suite *FiltersTestSuite) TestPutFilterUnalteredExpirationDateJSON() {
|
||||
id := suite.testFilters["local_account_1_filter_4"].ID
|
||||
|
||||
// Setup: set an expiration date for the filter.
|
||||
expiresIn := 86400
|
||||
filter := suite.setFilterExpiration(id, &expiresIn, nil, nil)
|
||||
if !suite.NotNil(filter.ExpiresAt) {
|
||||
suite.FailNow("Test precondition failed")
|
||||
}
|
||||
|
||||
// Update nothing. There should still be an expiration date.
|
||||
requestJson := `{}`
|
||||
filter = suite.setFilterExpiration(id, nil, nil, &requestJson)
|
||||
suite.NotNil(filter.ExpiresAt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// NotificationsGETHandler swagger:operation GET /api/v1/notifications notifications
|
||||
|
|
@ -152,27 +154,23 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
limit := 20
|
||||
limitString := c.Query(LimitKey)
|
||||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 32)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
page, errWithCode := paging.ParseIDPage(c,
|
||||
1, // min limit
|
||||
80, // max limit
|
||||
20, // no limit
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
resp, errWithCode := m.processor.Timeline().NotificationsGet(
|
||||
c.Request.Context(),
|
||||
ctx,
|
||||
authed,
|
||||
c.Query(MaxIDKey),
|
||||
c.Query(SinceIDKey),
|
||||
c.Query(MinIDKey),
|
||||
limit,
|
||||
c.QueryArray(TypesKey),
|
||||
c.QueryArray(ExcludeTypesKey),
|
||||
page,
|
||||
parseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types.
|
||||
parseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
|
|
@ -185,3 +183,28 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
||||
|
||||
// parseNotificationTypes converts the given slice of string values
|
||||
// to gtsmodel notification types, logging + skipping unknown types.
|
||||
func parseNotificationTypes(
|
||||
ctx context.Context,
|
||||
values []string,
|
||||
) []gtsmodel.NotificationType {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ntypes := make([]gtsmodel.NotificationType, 0, len(values))
|
||||
for _, value := range values {
|
||||
ntype := gtsmodel.ParseNotificationType(value)
|
||||
if ntype == gtsmodel.NotificationUnknown {
|
||||
// Type we don't know about (yet), log and ignore it.
|
||||
log.Warnf(ctx, "ignoring unknown type %s", value)
|
||||
continue
|
||||
}
|
||||
|
||||
ntypes = append(ntypes, ntype)
|
||||
}
|
||||
|
||||
return ntypes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,45 @@ func (suite *NotificationsTestSuite) TestGetNotificationsIncludeOneType() {
|
|||
}
|
||||
}
|
||||
|
||||
// Test including an unknown notification type, it should be ignored.
|
||||
func (suite *NotificationsTestSuite) TestGetNotificationsIncludeUnknownType() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
testToken := suite.testTokens["local_account_1"]
|
||||
testUser := suite.testUsers["local_account_1"]
|
||||
|
||||
suite.addMoreNotifications(testAccount)
|
||||
|
||||
maxID := ""
|
||||
minID := ""
|
||||
limit := 10
|
||||
types := []string{"favourite", "something.weird"}
|
||||
excludeTypes := []string(nil)
|
||||
expectedHTTPStatus := http.StatusOK
|
||||
expectedBody := ""
|
||||
|
||||
notifications, _, err := suite.getNotifications(
|
||||
testAccount,
|
||||
testToken,
|
||||
testUser,
|
||||
maxID,
|
||||
minID,
|
||||
limit,
|
||||
types,
|
||||
excludeTypes,
|
||||
expectedHTTPStatus,
|
||||
expectedBody,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// This should only include the fav notification.
|
||||
suite.Len(notifications, 1)
|
||||
for _, notification := range notifications {
|
||||
suite.Equal("favourite", notification.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookmarkTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(NotificationsTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ type Account struct {
|
|||
// Description of this account's avatar, for alt text.
|
||||
// example: A cute drawing of a smiling sloth.
|
||||
AvatarDescription string `json:"avatar_description,omitempty"`
|
||||
// Database ID of the media attachment for this account's avatar image.
|
||||
// Omitted if no avatar uploaded for this account (ie., default avatar).
|
||||
// example: 01JAJ3XCD66K3T99JZESCR137W
|
||||
AvatarMediaID string `json:"avatar_media_id,omitempty"`
|
||||
// Web location of the account's header image.
|
||||
// example: https://example.org/media/some_user/header/original/header.jpeg
|
||||
Header string `json:"header"`
|
||||
|
|
@ -78,14 +82,18 @@ type Account struct {
|
|||
// Description of this account's header, for alt text.
|
||||
// example: A sunlit field with purple flowers.
|
||||
HeaderDescription string `json:"header_description,omitempty"`
|
||||
// Database ID of the media attachment for this account's header image.
|
||||
// Omitted if no header uploaded for this account (ie., default header).
|
||||
// example: 01JAJ3XCD66K3T99JZESCR137W
|
||||
HeaderMediaID string `json:"header_media_id,omitempty"`
|
||||
// Number of accounts following this account, according to our instance.
|
||||
FollowersCount int `json:"followers_count"`
|
||||
// Number of account's followed by this account, according to our instance.
|
||||
FollowingCount int `json:"following_count"`
|
||||
// Number of statuses posted by this account, according to our instance.
|
||||
StatusesCount int `json:"statuses_count"`
|
||||
// When the account's most recent status was posted (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// When the account's most recent status was posted (ISO 8601 Date).
|
||||
// example: 2021-07-30
|
||||
LastStatusAt *string `json:"last_status_at"`
|
||||
// Array of custom emojis used in this account's note or display name.
|
||||
// Empty for blocked accounts.
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ type MediaDimensions struct {
|
|||
Duration float32 `json:"duration,omitempty"`
|
||||
// Bitrate of the media in bits per second.
|
||||
// example: 1000000
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
Bitrate uint64 `json:"bitrate,omitempty"`
|
||||
// Size of the media, in the format `[width]x[height]`.
|
||||
// Not set for audio.
|
||||
// example: 1920x1080
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ type Conversation struct {
|
|||
// Is the conversation currently marked as unread?
|
||||
Unread bool `json:"unread"`
|
||||
// Participants in the conversation.
|
||||
//
|
||||
// If this is a conversation between no accounts (ie., a self-directed DM),
|
||||
// this will include only the requesting account itself. Otherwise, it will
|
||||
// include every other account in the conversation *except* the requester.
|
||||
Accounts []Account `json:"accounts"`
|
||||
// The last status in the conversation. May be `null`.
|
||||
LastStatus *Status `json:"last_status"`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ type DomainPermission struct {
|
|||
// Time at which the permission entry was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
// Permission type of this entry (block, allow).
|
||||
// Only set for domain permission drafts.
|
||||
PermissionType string `json:"permission_type,omitempty"`
|
||||
}
|
||||
|
||||
// DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
|
||||
|
|
@ -69,22 +72,24 @@ type DomainPermission struct {
|
|||
type DomainPermissionRequest struct {
|
||||
// A list of domains for which this permission request should apply.
|
||||
// Only used if import=true is specified.
|
||||
Domains *multipart.FileHeader `form:"domains" json:"domains" xml:"domains"`
|
||||
Domains *multipart.FileHeader `form:"domains" json:"domains"`
|
||||
// A single domain for which this permission request should apply.
|
||||
// Only used if import=true is NOT specified or if import=false.
|
||||
// example: example.org
|
||||
Domain string `form:"domain" json:"domain" xml:"domain"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
// Obfuscate the domain name when displaying this permission entry publicly.
|
||||
// Ie., instead of 'example.org' show something like 'e**mpl*.or*'.
|
||||
// example: false
|
||||
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
|
||||
Obfuscate bool `form:"obfuscate" json:"obfuscate"`
|
||||
// Private comment for other admins on why this permission entry was created.
|
||||
// example: don't like 'em!!!!
|
||||
PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"`
|
||||
PrivateComment string `form:"private_comment" json:"private_comment"`
|
||||
// Public comment on why this permission entry was created.
|
||||
// Will be visible to requesters at /api/v1/instance/peers if this endpoint is exposed.
|
||||
// example: foss dorks 😫
|
||||
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
|
||||
PublicComment string `form:"public_comment" json:"public_comment"`
|
||||
// Permission type to create (only applies to domain permission drafts, not explicit blocks and allows).
|
||||
PermissionType string `form:"permission_type" json:"permission_type"`
|
||||
}
|
||||
|
||||
// DomainKeysExpireRequest is the form submitted as a POST to /api/v1/admin/domain_keys_expire to expire a domain's public keys.
|
||||
|
|
@ -92,5 +97,5 @@ type DomainPermissionRequest struct {
|
|||
// swagger:parameters domainKeysExpire
|
||||
type DomainKeysExpireRequest struct {
|
||||
// hostname/domain to expire keys for.
|
||||
Domain string `form:"domain" json:"domain" xml:"domain"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,5 +95,5 @@ type FilterCreateUpdateRequestV1 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ type FilterCreateRequestV2 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
|
||||
// Keywords to be added to the newly created filter.
|
||||
Keywords []FilterKeywordCreateUpdateRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
|
||||
|
|
@ -199,7 +199,7 @@ type FilterUpdateRequestV2 struct {
|
|||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
ExpiresInI Nullable[any] `json:"expires_in"`
|
||||
|
||||
// Keywords to be added to the filter, modified, or removed.
|
||||
Keywords []FilterKeywordCreateUpdateDeleteRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
|
||||
|
|
|
|||
107
internal/api/model/nullable.go
Normal file
107
internal/api/model/nullable.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Nullable is a generic type, which implements a field that can be one of three states:
|
||||
//
|
||||
// - field is not set in the request
|
||||
// - field is explicitly set to `null` in the request
|
||||
// - field is explicitly set to a valid value in the request
|
||||
//
|
||||
// Nullable is intended to be used with JSON unmarshalling.
|
||||
//
|
||||
// Adapted from https://github.com/oapi-codegen/nullable/blob/main/nullable.go
|
||||
type Nullable[T any] struct {
|
||||
state nullableState
|
||||
value T
|
||||
}
|
||||
|
||||
type nullableState uint8
|
||||
|
||||
const (
|
||||
nullableStateUnspecified nullableState = 0
|
||||
nullableStateNull nullableState = 1
|
||||
nullableStateSet nullableState = 2
|
||||
)
|
||||
|
||||
// Get retrieves the underlying value, if present,
|
||||
// and returns an error if the value was not present.
|
||||
func (t Nullable[T]) Get() (T, error) {
|
||||
var empty T
|
||||
if t.IsNull() {
|
||||
return empty, errors.New("value is null")
|
||||
}
|
||||
|
||||
if !t.IsSpecified() {
|
||||
return empty, errors.New("value is not specified")
|
||||
}
|
||||
|
||||
return t.value, nil
|
||||
}
|
||||
|
||||
// IsNull indicates whether the field
|
||||
// was sent, and had a value of `null`
|
||||
func (t Nullable[T]) IsNull() bool {
|
||||
return t.state == nullableStateNull
|
||||
}
|
||||
|
||||
// IsSpecified indicates whether the field
|
||||
// was sent either as a value or as `null`.
|
||||
func (t Nullable[T]) IsSpecified() bool {
|
||||
return t.state != nullableStateUnspecified
|
||||
}
|
||||
|
||||
// If field is unspecified,
|
||||
// UnmarshalJSON won't be called.
|
||||
func (t *Nullable[T]) UnmarshalJSON(data []byte) error {
|
||||
// If field is specified as `null`.
|
||||
if bytes.Equal(data, []byte("null")) {
|
||||
t.setNull()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, we have an
|
||||
// actual value, so parse it.
|
||||
var v T
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.set(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNull indicates that the field
|
||||
// was sent, and had a value of `null`
|
||||
func (t *Nullable[T]) setNull() {
|
||||
*t = Nullable[T]{state: nullableStateNull}
|
||||
}
|
||||
|
||||
// set the underlying value to given value.
|
||||
func (t *Nullable[T]) set(value T) {
|
||||
*t = Nullable[T]{
|
||||
state: nullableStateSet,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
88
internal/api/util/parseform.go
Normal file
88
internal/api/util/parseform.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// ParseDuration parses the given raw interface belonging
|
||||
// the given fieldName as an integer duration.
|
||||
func ParseDuration(rawI any, fieldName string) (*int, error) {
|
||||
var (
|
||||
asInteger int
|
||||
err error
|
||||
)
|
||||
|
||||
switch raw := rawI.(type) {
|
||||
case float64:
|
||||
// Submitted as JSON number
|
||||
// (casts to float64 by default).
|
||||
asInteger = int(raw)
|
||||
|
||||
case string:
|
||||
// Submitted as JSON string or form field.
|
||||
asInteger, err = strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(
|
||||
"could not parse %s value %s as integer: %w",
|
||||
fieldName, raw, err,
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
// Submitted as god-knows-what.
|
||||
err = fmt.Errorf(
|
||||
"could not parse %s type %T as integer",
|
||||
fieldName, rawI,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &asInteger, nil
|
||||
}
|
||||
|
||||
// ParseNullableDuration is like ParseDuration, but
|
||||
// for JSON values that may have been sent as `null`.
|
||||
//
|
||||
// IsSpecified should be checked and "true" on the
|
||||
// given nullable before calling this function.
|
||||
func ParseNullableDuration(
|
||||
nullable apimodel.Nullable[any],
|
||||
fieldName string,
|
||||
) (*int, error) {
|
||||
if nullable.IsNull() {
|
||||
// Was specified as `null`,
|
||||
// return pointer to zero value.
|
||||
return util.Ptr(0), nil
|
||||
}
|
||||
|
||||
rawI, err := nullable.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseDuration(rawI, fieldName)
|
||||
}
|
||||
|
|
@ -69,8 +69,11 @@ const (
|
|||
|
||||
/* Domain permission keys */
|
||||
|
||||
DomainPermissionExportKey = "export"
|
||||
DomainPermissionImportKey = "import"
|
||||
DomainPermissionExportKey = "export"
|
||||
DomainPermissionImportKey = "import"
|
||||
DomainPermissionSubscriptionIDKey = "subscription_id"
|
||||
DomainPermissionPermTypeKey = "permission_type"
|
||||
DomainPermissionDomainKey = "domain"
|
||||
|
||||
/* Admin query keys */
|
||||
|
||||
|
|
|
|||
2
internal/cache/cache.go
vendored
2
internal/cache/cache.go
vendored
|
|
@ -74,6 +74,8 @@ func (c *Caches) Init() {
|
|||
c.initConversationLastStatusIDs()
|
||||
c.initDomainAllow()
|
||||
c.initDomainBlock()
|
||||
c.initDomainPermissionDraft()
|
||||
c.initDomainPermissionExclude()
|
||||
c.initEmoji()
|
||||
c.initEmojiCategory()
|
||||
c.initFilter()
|
||||
|
|
|
|||
42
internal/cache/db.go
vendored
42
internal/cache/db.go
vendored
|
|
@ -67,6 +67,12 @@ type DBCaches struct {
|
|||
// DomainBlock provides access to the domain block database cache.
|
||||
DomainBlock *domain.Cache
|
||||
|
||||
// DomainPermissionDraft provides access to the domain permission draft database cache.
|
||||
DomainPermissionDraft StructCache[*gtsmodel.DomainPermissionDraft]
|
||||
|
||||
// DomainPermissionExclude provides access to the domain permission exclude database cache.
|
||||
DomainPermissionExclude *domain.Cache
|
||||
|
||||
// Emoji provides access to the gtsmodel Emoji database cache.
|
||||
Emoji StructCache[*gtsmodel.Emoji]
|
||||
|
||||
|
|
@ -548,6 +554,42 @@ func (c *Caches) initDomainBlock() {
|
|||
c.DB.DomainBlock = new(domain.Cache)
|
||||
}
|
||||
|
||||
func (c *Caches) initDomainPermissionDraft() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofDomainPermissionDraft(), // model in-mem size.
|
||||
config.GetCacheDomainPermissionDraftMemRation(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "cache size = %d", cap)
|
||||
|
||||
copyF := func(d1 *gtsmodel.DomainPermissionDraft) *gtsmodel.DomainPermissionDraft {
|
||||
d2 := new(gtsmodel.DomainPermissionDraft)
|
||||
*d2 = *d1
|
||||
|
||||
// Don't include ptr fields that
|
||||
// will be populated separately.
|
||||
d2.CreatedByAccount = nil
|
||||
|
||||
return d2
|
||||
}
|
||||
|
||||
c.DB.DomainPermissionDraft.Init(structr.CacheConfig[*gtsmodel.DomainPermissionDraft]{
|
||||
Indices: []structr.IndexConfig{
|
||||
{Fields: "ID"},
|
||||
{Fields: "Domain", Multiple: true},
|
||||
{Fields: "SubscriptionID", Multiple: true},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
Copy: copyF,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Caches) initDomainPermissionExclude() {
|
||||
c.DB.DomainPermissionExclude = new(domain.Cache)
|
||||
}
|
||||
|
||||
func (c *Caches) initEmoji() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
|
|
|
|||
2
internal/cache/domain/domain.go
vendored
2
internal/cache/domain/domain.go
vendored
|
|
@ -220,7 +220,7 @@ func (n *node) getChild(part string) *node {
|
|||
|
||||
for i < j {
|
||||
// avoid overflow when computing h
|
||||
h := int(uint(i+j) >> 1)
|
||||
h := int(uint(i+j) >> 1) // #nosec G115
|
||||
// i ≤ h < j
|
||||
|
||||
if n.child[h].part < part {
|
||||
|
|
|
|||
15
internal/cache/size.go
vendored
15
internal/cache/size.go
vendored
|
|
@ -342,6 +342,21 @@ func sizeofConversation() uintptr {
|
|||
}))
|
||||
}
|
||||
|
||||
func sizeofDomainPermissionDraft() uintptr {
|
||||
return uintptr(size.Of(>smodel.DomainPermissionDraft{
|
||||
ID: exampleID,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
Domain: "example.org",
|
||||
CreatedByAccountID: exampleID,
|
||||
PrivateComment: exampleTextSmall,
|
||||
PublicComment: exampleTextSmall,
|
||||
Obfuscate: util.Ptr(false),
|
||||
SubscriptionID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofEmoji() uintptr {
|
||||
return uintptr(size.Of(>smodel.Emoji{
|
||||
ID: exampleID,
|
||||
|
|
|
|||
2
internal/cache/util.go
vendored
2
internal/cache/util.go
vendored
|
|
@ -18,7 +18,6 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
|
|
@ -42,7 +41,6 @@ func ignoreErrors(err error) bool {
|
|||
// (until invalidation).
|
||||
db.ErrNoEntries,
|
||||
db.ErrAlreadyExists,
|
||||
sql.ErrNoRows,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
12
internal/cache/visibility.go
vendored
12
internal/cache/visibility.go
vendored
|
|
@ -48,9 +48,15 @@ func (c *Caches) initVisibility() {
|
|||
{Fields: "RequesterID", Multiple: true},
|
||||
{Fields: "Type,RequesterID,ItemID"},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
Copy: copyF,
|
||||
MaxSize: cap,
|
||||
IgnoreErr: func(err error) bool {
|
||||
// don't cache any errors,
|
||||
// it gets a little too tricky
|
||||
// otherwise with ensuring
|
||||
// errors are cleared out
|
||||
return true
|
||||
},
|
||||
Copy: copyF,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ type Configuration struct {
|
|||
MediaRemoteCacheDays int `name:"media-remote-cache-days" usage:"Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely."`
|
||||
MediaEmojiLocalMaxSize bytesize.Size `name:"media-emoji-local-max-size" usage:"Max size in bytes of emojis uploaded to this instance via the admin API."`
|
||||
MediaEmojiRemoteMaxSize bytesize.Size `name:"media-emoji-remote-max-size" usage:"Max size in bytes of emojis to download from other instances."`
|
||||
MediaImageSizeHint bytesize.Size `name:"media-image-size-hint" usage:"Size in bytes of max image size referred to on /api/v_/instance endpoints (else, local max size)"`
|
||||
MediaVideoSizeHint bytesize.Size `name:"media-video-size-hint" usage:"Size in bytes of max video size referred to on /api/v_/instance endpoints (else, local max size)"`
|
||||
MediaLocalMaxSize bytesize.Size `name:"media-local-max-size" usage:"Max size in bytes of media uploaded to this instance via API"`
|
||||
MediaRemoteMaxSize bytesize.Size `name:"media-remote-max-size" usage:"Max size in bytes of media to download from other instances"`
|
||||
MediaCleanupFrom string `name:"media-cleanup-from" usage:"Time of day from which to start running media cleanup/prune jobs. Should be in the format 'hh:mm:ss', eg., '15:04:05'."`
|
||||
|
|
@ -206,6 +208,7 @@ type CacheConfiguration struct {
|
|||
ClientMemRatio float64 `name:"client-mem-ratio"`
|
||||
ConversationMemRatio float64 `name:"conversation-mem-ratio"`
|
||||
ConversationLastStatusIDsMemRatio float64 `name:"conversation-last-status-ids-mem-ratio"`
|
||||
DomainPermissionDraftMemRation float64 `name:"domain-permission-draft-mem-ratio"`
|
||||
EmojiMemRatio float64 `name:"emoji-mem-ratio"`
|
||||
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
|
||||
FilterMemRatio float64 `name:"filter-mem-ratio"`
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ var Defaults = Configuration{
|
|||
ClientMemRatio: 0.1,
|
||||
ConversationMemRatio: 1,
|
||||
ConversationLastStatusIDsMemRatio: 2,
|
||||
DomainPermissionDraftMemRation: 0.5,
|
||||
EmojiMemRatio: 3,
|
||||
EmojiCategoryMemRatio: 0.1,
|
||||
FilterMemRatio: 0.5,
|
||||
|
|
|
|||
|
|
@ -1225,6 +1225,56 @@ func GetMediaEmojiRemoteMaxSize() bytesize.Size { return global.GetMediaEmojiRem
|
|||
// SetMediaEmojiRemoteMaxSize safely sets the value for global configuration 'MediaEmojiRemoteMaxSize' field
|
||||
func SetMediaEmojiRemoteMaxSize(v bytesize.Size) { global.SetMediaEmojiRemoteMaxSize(v) }
|
||||
|
||||
// GetMediaImageSizeHint safely fetches the Configuration value for state's 'MediaImageSizeHint' field
|
||||
func (st *ConfigState) GetMediaImageSizeHint() (v bytesize.Size) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.MediaImageSizeHint
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetMediaImageSizeHint safely sets the Configuration value for state's 'MediaImageSizeHint' field
|
||||
func (st *ConfigState) SetMediaImageSizeHint(v bytesize.Size) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.MediaImageSizeHint = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// MediaImageSizeHintFlag returns the flag name for the 'MediaImageSizeHint' field
|
||||
func MediaImageSizeHintFlag() string { return "media-image-size-hint" }
|
||||
|
||||
// GetMediaImageSizeHint safely fetches the value for global configuration 'MediaImageSizeHint' field
|
||||
func GetMediaImageSizeHint() bytesize.Size { return global.GetMediaImageSizeHint() }
|
||||
|
||||
// SetMediaImageSizeHint safely sets the value for global configuration 'MediaImageSizeHint' field
|
||||
func SetMediaImageSizeHint(v bytesize.Size) { global.SetMediaImageSizeHint(v) }
|
||||
|
||||
// GetMediaVideoSizeHint safely fetches the Configuration value for state's 'MediaVideoSizeHint' field
|
||||
func (st *ConfigState) GetMediaVideoSizeHint() (v bytesize.Size) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.MediaVideoSizeHint
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetMediaVideoSizeHint safely sets the Configuration value for state's 'MediaVideoSizeHint' field
|
||||
func (st *ConfigState) SetMediaVideoSizeHint(v bytesize.Size) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.MediaVideoSizeHint = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// MediaVideoSizeHintFlag returns the flag name for the 'MediaVideoSizeHint' field
|
||||
func MediaVideoSizeHintFlag() string { return "media-video-size-hint" }
|
||||
|
||||
// GetMediaVideoSizeHint safely fetches the value for global configuration 'MediaVideoSizeHint' field
|
||||
func GetMediaVideoSizeHint() bytesize.Size { return global.GetMediaVideoSizeHint() }
|
||||
|
||||
// SetMediaVideoSizeHint safely sets the value for global configuration 'MediaVideoSizeHint' field
|
||||
func SetMediaVideoSizeHint(v bytesize.Size) { global.SetMediaVideoSizeHint(v) }
|
||||
|
||||
// GetMediaLocalMaxSize safely fetches the Configuration value for state's 'MediaLocalMaxSize' field
|
||||
func (st *ConfigState) GetMediaLocalMaxSize() (v bytesize.Size) {
|
||||
st.mutex.RLock()
|
||||
|
|
@ -3106,6 +3156,37 @@ func SetCacheConversationLastStatusIDsMemRatio(v float64) {
|
|||
global.SetCacheConversationLastStatusIDsMemRatio(v)
|
||||
}
|
||||
|
||||
// GetCacheDomainPermissionDraftMemRation safely fetches the Configuration value for state's 'Cache.DomainPermissionDraftMemRation' field
|
||||
func (st *ConfigState) GetCacheDomainPermissionDraftMemRation() (v float64) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.DomainPermissionDraftMemRation
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheDomainPermissionDraftMemRation safely sets the Configuration value for state's 'Cache.DomainPermissionDraftMemRation' field
|
||||
func (st *ConfigState) SetCacheDomainPermissionDraftMemRation(v float64) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.DomainPermissionDraftMemRation = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheDomainPermissionDraftMemRationFlag returns the flag name for the 'Cache.DomainPermissionDraftMemRation' field
|
||||
func CacheDomainPermissionDraftMemRationFlag() string {
|
||||
return "cache-domain-permission-draft-mem-ratio"
|
||||
}
|
||||
|
||||
// GetCacheDomainPermissionDraftMemRation safely fetches the value for global configuration 'Cache.DomainPermissionDraftMemRation' field
|
||||
func GetCacheDomainPermissionDraftMemRation() float64 {
|
||||
return global.GetCacheDomainPermissionDraftMemRation()
|
||||
}
|
||||
|
||||
// SetCacheDomainPermissionDraftMemRation safely sets the value for global configuration 'Cache.DomainPermissionDraftMemRation' field
|
||||
func SetCacheDomainPermissionDraftMemRation(v float64) {
|
||||
global.SetCacheDomainPermissionDraftMemRation(v)
|
||||
}
|
||||
|
||||
// GetCacheEmojiMemRatio safely fetches the Configuration value for state's 'Cache.EmojiMemRatio' field
|
||||
func (st *ConfigState) GetCacheEmojiMemRatio() (v float64) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -86,7 +87,7 @@ func (a *accountDB) GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsm
|
|||
// Reorder the statuses by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(a *gtsmodel.Account) string { return a.ID }
|
||||
util.OrderBy(accounts, ids, getID)
|
||||
xslices.OrderBy(accounts, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status
|
|||
status.URI = fmt.Sprintf("http://localhost:8080/users/%s/statuses/%s", testAccount.Username, status.ID)
|
||||
status.Local = util.Ptr(true)
|
||||
|
||||
if status.Visibility == "" {
|
||||
if status.Visibility == 0 {
|
||||
status.Visibility = gtsmodel.VisibilityDefault
|
||||
}
|
||||
if status.ActivityStreamsType == "" {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ func (a *applicationDB) GetAllTokens(ctx context.Context) ([]*gtsmodel.Token, er
|
|||
// Reoroder the tokens by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(t *gtsmodel.Token) string { return t.ID }
|
||||
util.OrderBy(tokens, tokenIDs, getID)
|
||||
xslices.OrderBy(tokens, tokenIDs, getID)
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
|
|
@ -48,6 +49,7 @@ import (
|
|||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
// DBService satisfies the DB interface
|
||||
|
|
@ -130,18 +132,18 @@ func doMigration(ctx context.Context, db *bun.DB) error {
|
|||
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
|
||||
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
|
||||
func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||
var db *bun.DB
|
||||
var sqldb *sql.DB
|
||||
var dialect func() schema.Dialect
|
||||
var err error
|
||||
t := strings.ToLower(config.GetDbType())
|
||||
|
||||
switch t {
|
||||
switch t := strings.ToLower(config.GetDbType()); t {
|
||||
case "postgres":
|
||||
db, err = pgConn(ctx)
|
||||
sqldb, dialect, err = pgConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "sqlite":
|
||||
db, err = sqliteConn(ctx)
|
||||
sqldb, dialect, err = sqliteConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -149,34 +151,20 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return nil, fmt.Errorf("database type %s not supported for bundb", t)
|
||||
}
|
||||
|
||||
// Add database query hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
// perform any pending database migrations: this includes
|
||||
// the very first 'migration' on startup which just creates
|
||||
// necessary tables
|
||||
if err := doMigration(ctx, db); err != nil {
|
||||
// perform any pending database migrations: this includes the first
|
||||
// 'migration' on startup which just creates necessary db tables.
|
||||
//
|
||||
// Note this uses its own instance of bun.DB as bun will automatically
|
||||
// store in-memory reflect type schema of any Go models passed to it,
|
||||
// and we still maintain lots of old model versions in the migrations.
|
||||
if err := doMigration(ctx, bunDB(sqldb, dialect)); err != nil {
|
||||
return nil, fmt.Errorf("db migration error: %s", err)
|
||||
}
|
||||
|
||||
// Wrap sql.DB as bun.DB type,
|
||||
// adding any connection hooks.
|
||||
db := bunDB(sqldb, dialect)
|
||||
|
||||
ps := &DBService{
|
||||
Account: &accountDB{
|
||||
db: db,
|
||||
|
|
@ -318,17 +306,47 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
return ps, nil
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*bun.DB, error) {
|
||||
// bunDB returns a new bun.DB for given sql.DB connection pool and dialect
|
||||
// function. This can be used to apply any necessary opts / hooks as we
|
||||
// initialize a bun.DB object both before and after performing migrations.
|
||||
func bunDB(sqldb *sql.DB, dialect func() schema.Dialect) *bun.DB {
|
||||
db := bun.NewDB(sqldb, dialect())
|
||||
|
||||
// Add our SQL connection hooks.
|
||||
db.AddQueryHook(queryHook{})
|
||||
if config.GetTracingEnabled() {
|
||||
db.AddQueryHook(tracing.InstrumentBun())
|
||||
}
|
||||
if config.GetMetricsEnabled() {
|
||||
db.AddQueryHook(metrics.InstrumentBun())
|
||||
}
|
||||
|
||||
// table registration is needed for many-to-many, see:
|
||||
// https://bun.uptrace.dev/orm/many-to-many-relation/
|
||||
for _, t := range []interface{}{
|
||||
>smodel.AccountToEmoji{},
|
||||
>smodel.ConversationToStatus{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.ThreadToStatus{},
|
||||
} {
|
||||
db.RegisterModel(t)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func pgConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
opts, err := deriveBunDBPGOptions() //nolint:contextcheck
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not create bundb postgres options: %w", err)
|
||||
}
|
||||
|
||||
cfg := stdlib.RegisterConnConfig(opts)
|
||||
|
||||
sqldb, err := sql.Open("pgx-gts", cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
return nil, nil, fmt.Errorf("could not open postgres db: %w", err)
|
||||
}
|
||||
|
||||
// Tune db connections for postgres, see:
|
||||
|
|
@ -338,22 +356,20 @@ func pgConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetMaxIdleConns(2) // assume default 2; if max idle is less than max open, it will be automatically adjusted
|
||||
sqldb.SetConnMaxLifetime(5 * time.Minute) // fine to kill old connections
|
||||
|
||||
db := bun.NewDB(sqldb, pgdialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("postgres ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("postgres ping: %w", err)
|
||||
}
|
||||
|
||||
log.Info(ctx, "connected to POSTGRES database")
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return pgdialect.New() }, nil
|
||||
}
|
||||
|
||||
func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
||||
func sqliteConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
|
||||
// validate db address has actually been set
|
||||
address := config.GetDbAddress()
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
|
||||
}
|
||||
|
||||
// Build SQLite connection address with prefs.
|
||||
|
|
@ -362,7 +378,7 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
// Open new DB instance
|
||||
sqldb, err := sql.Open("sqlite-gts", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
return nil, nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
|
||||
}
|
||||
|
||||
// Tune db connections for sqlite, see:
|
||||
|
|
@ -378,16 +394,14 @@ func sqliteConn(ctx context.Context) (*bun.DB, error) {
|
|||
sqldb.SetConnMaxLifetime(5 * time.Minute)
|
||||
}
|
||||
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
||||
// ping to check the db is there and listening
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return nil, nil, fmt.Errorf("sqlite ping: %w", err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "connected to SQLITE database with address %s", address)
|
||||
|
||||
return db, nil
|
||||
return sqldb, func() schema.Dialect { return sqlitedialect.New() }, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -401,19 +415,30 @@ func maxOpenConns() int {
|
|||
if multiplier < 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Specifically for SQLite databases with
|
||||
// a journal mode of anything EXCEPT "wal",
|
||||
// only 1 concurrent connection is supported.
|
||||
if strings.ToLower(config.GetDbType()) == "sqlite" {
|
||||
journalMode := config.GetDbSqliteJournalMode()
|
||||
journalMode = strings.ToLower(journalMode)
|
||||
if journalMode != "wal" {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier * runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
// deriveBunDBPGOptions takes an application config and returns either a ready-to-use set of options
|
||||
// with sensible defaults, or an error if it's not satisfied by the provided config.
|
||||
func deriveBunDBPGOptions() (*pgx.ConnConfig, error) {
|
||||
url := config.GetDbPostgresConnectionString()
|
||||
|
||||
// if database URL is defined, ignore other DB related configuration fields
|
||||
if url != "" {
|
||||
cfg, err := pgx.ParseConfig(url)
|
||||
return cfg, err
|
||||
// If database URL is defined, ignore
|
||||
// other DB-related configuration fields.
|
||||
if url := config.GetDbPostgresConnectionString(); url != "" {
|
||||
return pgx.ParseConfig(url)
|
||||
}
|
||||
|
||||
// these are all optional, the db adapter figures out defaults
|
||||
address := config.GetDbAddress()
|
||||
|
||||
|
|
@ -477,7 +502,10 @@ func deriveBunDBPGOptions() (*pgx.ConnConfig, error) {
|
|||
cfg.Host = address
|
||||
}
|
||||
if port := config.GetDbPort(); port > 0 {
|
||||
cfg.Port = uint16(port)
|
||||
if port > math.MaxUint16 {
|
||||
return nil, errors.New("invalid port, must be in range 1-65535")
|
||||
}
|
||||
cfg.Port = uint16(port) // #nosec G115 -- Just validated above.
|
||||
}
|
||||
if u := config.GetDbUser(); u != "" {
|
||||
cfg.User = u
|
||||
|
|
@ -502,15 +530,12 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
//
|
||||
// - SQLite by itself supports setting a subset of its configuration options
|
||||
// via URI query arguments in the connection. Namely `mode` and `cache`.
|
||||
// This is the same situation for the directly transpiled C->Go code in
|
||||
// modernc.org/sqlite, i.e. modernc.org/sqlite/lib, NOT the Go SQL driver.
|
||||
// This is the same situation for our supported SQLite implementations.
|
||||
//
|
||||
// - `modernc.org/sqlite` has a "shim" around it to allow the directly
|
||||
// transpiled C code to be usable with a more native Go API. This is in
|
||||
// the form of a `database/sql/driver.Driver{}` implementation that calls
|
||||
// through to the transpiled C code.
|
||||
// - Both implementations have a "shim" around them in the form of a
|
||||
// `database/sql/driver.Driver{}` implementation.
|
||||
//
|
||||
// - The SQLite shim we interface with adds support for setting ANY of the
|
||||
// - The SQLite shims we interface with add support for setting ANY of the
|
||||
// configuration options via query arguments, through using a special `_pragma`
|
||||
// query key that specifies SQLite PRAGMAs to set upon opening each connection.
|
||||
// As such you will see below that most config is set with the `_pragma` key.
|
||||
|
|
@ -536,12 +561,6 @@ func buildSQLiteAddress(addr string) (string, bool) {
|
|||
// reached. And for whatever reason (:shrug:) SQLite is very particular about
|
||||
// setting this BEFORE the `journal_mode` is set, otherwise you can end up
|
||||
// running into more of these `SQLITE_BUSY` return codes than you might expect.
|
||||
//
|
||||
// - One final thing (I promise!): `SQLITE_BUSY` is only handled by the internal
|
||||
// `busy_timeout` handler in the case that a data race occurs contending for
|
||||
// table locks. THERE ARE STILL OTHER SITUATIONS IN WHICH THIS MAY BE RETURNED!
|
||||
// As such, we use our wrapping DB{} and Tx{} types (in "db.go") which make use
|
||||
// of our own retry-busy handler.
|
||||
|
||||
// Drop anything fancy from DB address
|
||||
addr = strings.Split(addr, "?")[0] // drop any provided query strings
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -209,7 +209,7 @@ func (c *conversationDB) getConversationsByLastStatusIDs(
|
|||
|
||||
// Reorder the conversations by their last status IDs to ensure correct order.
|
||||
getID := func(b *gtsmodel.Conversation) string { return b.ID }
|
||||
util.OrderBy(conversations, conversationLastStatusIDs, getID)
|
||||
xslices.OrderBy(conversations, conversationLastStatusIDs, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -558,7 +558,7 @@ func (c *conversationDB) DeleteStatusFromConversations(ctx context.Context, stat
|
|||
|
||||
// Invalidate cache entries.
|
||||
updatedConversationIDs = append(updatedConversationIDs, deletedConversationIDs...)
|
||||
updatedConversationIDs = util.Deduplicate(updatedConversationIDs)
|
||||
updatedConversationIDs = xslices.Deduplicate(updatedConversationIDs)
|
||||
c.state.Caches.DB.Conversation.InvalidateIDs("ID", updatedConversationIDs)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
|
@ -110,6 +111,36 @@ func (d *domainDB) GetDomainAllowByID(ctx context.Context, id string) (*gtsmodel
|
|||
return &allow, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) UpdateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow, columns ...string) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
allow.Domain, err = util.Punify(allow.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure updated_at is set.
|
||||
allow.UpdatedAt = time.Now()
|
||||
if len(columns) != 0 {
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
// Attempt to update domain allow.
|
||||
if _, err := d.db.
|
||||
NewUpdate().
|
||||
Model(allow).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("domain_allow.id"), allow.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain allow cache (for later reload)
|
||||
d.state.Caches.DB.DomainAllow.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
|
||||
// Normalize the domain as punycode
|
||||
domain, err := util.Punify(domain)
|
||||
|
|
@ -206,6 +237,36 @@ func (d *domainDB) GetDomainBlockByID(ctx context.Context, id string) (*gtsmodel
|
|||
return &block, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) UpdateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock, columns ...string) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
block.Domain, err = util.Punify(block.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure updated_at is set.
|
||||
block.UpdatedAt = time.Now()
|
||||
if len(columns) != 0 {
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
// Attempt to update domain block.
|
||||
if _, err := d.db.
|
||||
NewUpdate().
|
||||
Model(block).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("domain_block.id"), block.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain block cache (for later reload)
|
||||
d.state.Caches.DB.DomainBlock.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) error {
|
||||
// Normalize the domain as punycode
|
||||
domain, err := util.Punify(domain)
|
||||
|
|
|
|||
285
internal/db/bundb/domainpermissiondraft.go
Normal file
285
internal/db/bundb/domainpermissiondraft.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func (d *domainDB) getDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
dbQuery func(*gtsmodel.DomainPermissionDraft) error,
|
||||
keyParts ...any,
|
||||
) (*gtsmodel.DomainPermissionDraft, error) {
|
||||
// Fetch perm draft from database cache with loader callback.
|
||||
permDraft, err := d.state.Caches.DB.DomainPermissionDraft.LoadOne(
|
||||
lookup,
|
||||
// Only called if not cached.
|
||||
func() (*gtsmodel.DomainPermissionDraft, error) {
|
||||
var permDraft gtsmodel.DomainPermissionDraft
|
||||
if err := dbQuery(&permDraft); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &permDraft, nil
|
||||
},
|
||||
keyParts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// No need to fully populate.
|
||||
return permDraft, nil
|
||||
}
|
||||
|
||||
if permDraft.CreatedByAccount == nil {
|
||||
// Not set, fetch from database.
|
||||
permDraft.CreatedByAccount, err = d.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
permDraft.CreatedByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error populating created by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return permDraft, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionDraftByID(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*gtsmodel.DomainPermissionDraft, error) {
|
||||
return d.getDomainPermissionDraft(
|
||||
ctx,
|
||||
"ID",
|
||||
func(permDraft *gtsmodel.DomainPermissionDraft) error {
|
||||
return d.db.
|
||||
NewSelect().
|
||||
Model(permDraft).
|
||||
Where("? = ?", bun.Ident("domain_permission_draft.id"), id).
|
||||
Scan(ctx)
|
||||
},
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionDrafts(
|
||||
ctx context.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
permSubID string,
|
||||
domain string,
|
||||
page *paging.Page,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionDraft,
|
||||
error,
|
||||
) {
|
||||
var (
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
permDraftIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_drafts"),
|
||||
bun.Ident("domain_permission_draft"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_draft.id")
|
||||
|
||||
// Return only items with id
|
||||
// lower than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where(
|
||||
"? < ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
maxID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with id
|
||||
// greater than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where(
|
||||
"? > ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
minID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with
|
||||
// given permission type.
|
||||
if permType != gtsmodel.DomainPermissionUnknown {
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.permission_type"),
|
||||
permType,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with
|
||||
// given subscription ID.
|
||||
if permSubID != "" {
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.subscription_id"),
|
||||
permSubID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items
|
||||
// with given domain.
|
||||
if domain != "" {
|
||||
var err error
|
||||
|
||||
// Normalize domain as punycode.
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.domain"),
|
||||
domain,
|
||||
)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
// Limit amount of
|
||||
// items returned.
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.OrderExpr(
|
||||
"? ASC",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
)
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &permDraftIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(permDraftIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// If we're paging up, we still want items
|
||||
// to be sorted by ID desc, so reverse slice.
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(permDraftIDs)
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permDraftIDs)
|
||||
permDrafts := make([]*gtsmodel.DomainPermissionDraft, 0, len(permDraftIDs))
|
||||
for _, id := range permDraftIDs {
|
||||
permDraft, err := d.GetDomainPermissionDraftByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission draft %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
permDrafts = append(permDrafts, permDraft)
|
||||
}
|
||||
|
||||
return permDrafts, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) PutDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
permDraft *gtsmodel.DomainPermissionDraft,
|
||||
) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode
|
||||
permDraft.Domain, err = util.Punify(permDraft.Domain)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error punifying domain %s: %w", permDraft.Domain, err)
|
||||
}
|
||||
|
||||
return d.state.Caches.DB.DomainPermissionDraft.Store(
|
||||
permDraft,
|
||||
func() error {
|
||||
_, err := d.db.
|
||||
NewInsert().
|
||||
Model(permDraft).
|
||||
Exec(ctx)
|
||||
return err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainPermissionDraft(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) error {
|
||||
// Delete the permDraft from DB.
|
||||
q := d.db.NewDelete().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_drafts"),
|
||||
bun.Ident("domain_permission_draft"),
|
||||
).
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_draft.id"),
|
||||
id,
|
||||
)
|
||||
|
||||
_, err := q.Exec(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate any cached model by ID.
|
||||
d.state.Caches.DB.DomainPermissionDraft.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
120
internal/db/bundb/domainpermissiondraft_test.go
Normal file
120
internal/db/bundb/domainpermissiondraft_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
type DomainPermissionDraftTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionDraftTestSuite) TestPermDraftCreateGetDelete() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
draft = >smodel.DomainPermissionDraft{
|
||||
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
PermissionType: gtsmodel.DomainPermissionBlock,
|
||||
Domain: "exämple.org",
|
||||
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
|
||||
PrivateComment: "this domain is poo",
|
||||
PublicComment: "this domain is poo, but phrased in a more outward-facing way",
|
||||
Obfuscate: util.Ptr(false),
|
||||
SubscriptionID: "01JCZN8PG55KKEVTDAY52D0T3P",
|
||||
}
|
||||
)
|
||||
|
||||
// Whack the draft in.
|
||||
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Get the draft again.
|
||||
dbDraft, err := suite.state.DB.GetDomainPermissionDraftByID(ctx, draft.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Domain should have been stored punycoded.
|
||||
suite.Equal("xn--exmple-cua.org", dbDraft.Domain)
|
||||
|
||||
// Search for domain using both
|
||||
// punycode and unicode variants.
|
||||
search1, err := suite.state.DB.GetDomainPermissionDrafts(
|
||||
ctx,
|
||||
gtsmodel.DomainPermissionUnknown,
|
||||
"",
|
||||
"exämple.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search1) != 1 {
|
||||
suite.FailNow("couldn't get domain perm draft exämple.org")
|
||||
}
|
||||
|
||||
search2, err := suite.state.DB.GetDomainPermissionDrafts(
|
||||
ctx,
|
||||
gtsmodel.DomainPermissionUnknown,
|
||||
"",
|
||||
"xn--exmple-cua.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search2) != 1 {
|
||||
suite.FailNow("couldn't get domain perm draft example.org")
|
||||
}
|
||||
|
||||
// Change ID + try to put the same draft again.
|
||||
draft.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
|
||||
err = suite.state.DB.PutDomainPermissionDraft(ctx, draft)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("was able to insert same domain perm draft twice")
|
||||
}
|
||||
|
||||
// Put same draft but change permission type, should work.
|
||||
draft.PermissionType = gtsmodel.DomainPermissionAllow
|
||||
if err := suite.state.DB.PutDomainPermissionDraft(ctx, draft); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Delete both drafts.
|
||||
for _, id := range []string{
|
||||
"01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
"01JCZNVYSDT3JE385FABMJ7ADQ",
|
||||
} {
|
||||
if err := suite.state.DB.DeleteDomainPermissionDraft(ctx, id); err != nil {
|
||||
suite.FailNow("error deleting domain permission draft")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainPermissionDraftTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DomainPermissionDraftTestSuite))
|
||||
}
|
||||
270
internal/db/bundb/domainpermissionexclude.go
Normal file
270
internal/db/bundb/domainpermissionexclude.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func (d *domainDB) PutDomainPermissionExclude(
|
||||
ctx context.Context,
|
||||
exclude *gtsmodel.DomainPermissionExclude,
|
||||
) error {
|
||||
// Normalize the domain as punycode
|
||||
var err error
|
||||
exclude.Domain, err = util.Punify(exclude.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attempt to store domain perm exclude in DB
|
||||
if _, err := d.db.NewInsert().
|
||||
Model(exclude).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain perm exclude cache (for later reload)
|
||||
d.state.Caches.DB.DomainPermissionExclude.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) IsDomainPermissionExcluded(ctx context.Context, domain string) (bool, error) {
|
||||
// Normalize the domain as punycode
|
||||
domain, err := util.Punify(domain)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Func to scan list of all
|
||||
// excluded domain perms from DB.
|
||||
loadF := func() ([]string, error) {
|
||||
var domains []string
|
||||
|
||||
if err := d.db.
|
||||
NewSelect().
|
||||
Table("domain_permission_excludes").
|
||||
Column("domain").
|
||||
Scan(ctx, &domains); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Exclude our own domain as creating blocks
|
||||
// or allows for self will likely break things.
|
||||
domains = append(domains, config.GetHost())
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
// Check the cache for a domain perm exclude,
|
||||
// hydrating the cache with loadF if necessary.
|
||||
return d.state.Caches.DB.DomainPermissionExclude.Matches(domain, loadF)
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionExcludeByID(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*gtsmodel.DomainPermissionExclude, error) {
|
||||
exclude := new(gtsmodel.DomainPermissionExclude)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
Model(exclude).
|
||||
Where("? = ?", bun.Ident("domain_permission_exclude.id"), id)
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// No need to fully populate.
|
||||
return exclude, nil
|
||||
}
|
||||
|
||||
if exclude.CreatedByAccount == nil {
|
||||
// Not set, fetch from database.
|
||||
var err error
|
||||
exclude.CreatedByAccount, err = d.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
exclude.CreatedByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error populating created by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return exclude, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) GetDomainPermissionExcludes(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
page *paging.Page,
|
||||
) (
|
||||
[]*gtsmodel.DomainPermissionExclude,
|
||||
error,
|
||||
) {
|
||||
var (
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
excludeIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := d.db.
|
||||
NewSelect().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_excludes"),
|
||||
bun.Ident("domain_permission_exclude"),
|
||||
).
|
||||
// Select only IDs from table
|
||||
Column("domain_permission_exclude.id")
|
||||
|
||||
// Return only items with id
|
||||
// lower than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where(
|
||||
"? < ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
maxID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items with id
|
||||
// greater than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where(
|
||||
"? > ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
minID,
|
||||
)
|
||||
}
|
||||
|
||||
// Return only items
|
||||
// with given domain.
|
||||
if domain != "" {
|
||||
var err error
|
||||
|
||||
// Normalize domain as punycode.
|
||||
domain, err = util.Punify(domain)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
q = q.Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_exclude.domain"),
|
||||
domain,
|
||||
)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
// Limit amount of
|
||||
// items returned.
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.OrderExpr(
|
||||
"? ASC",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
)
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.OrderExpr(
|
||||
"? DESC",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &excludeIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Catch case of no items early
|
||||
if len(excludeIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
// If we're paging up, we still want items
|
||||
// to be sorted by ID desc, so reverse slice.
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(excludeIDs)
|
||||
}
|
||||
|
||||
// Allocate return slice (will be at most len permSubIDs).
|
||||
excludes := make([]*gtsmodel.DomainPermissionExclude, 0, len(excludeIDs))
|
||||
for _, id := range excludeIDs {
|
||||
exclude, err := d.GetDomainPermissionExcludeByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting domain permission exclude %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to return slice
|
||||
excludes = append(excludes, exclude)
|
||||
}
|
||||
|
||||
return excludes, nil
|
||||
}
|
||||
|
||||
func (d *domainDB) DeleteDomainPermissionExclude(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) error {
|
||||
// Delete the permSub from DB.
|
||||
q := d.db.NewDelete().
|
||||
TableExpr(
|
||||
"? AS ?",
|
||||
bun.Ident("domain_permission_excludes"),
|
||||
bun.Ident("domain_permission_exclude"),
|
||||
).
|
||||
Where(
|
||||
"? = ?",
|
||||
bun.Ident("domain_permission_exclude.id"),
|
||||
id,
|
||||
)
|
||||
|
||||
_, err := q.Exec(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the domain perm exclude cache (for later reload)
|
||||
d.state.Caches.DB.DomainPermissionExclude.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
185
internal/db/bundb/domainpermissionexclude_test.go
Normal file
185
internal/db/bundb/domainpermissionexclude_test.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type DomainPermissionExcludeTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionExcludeTestSuite) TestPermExcludeCreateGetDelete() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
exclude = >smodel.DomainPermissionExclude{
|
||||
ID: "01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
Domain: "exämple.org",
|
||||
CreatedByAccountID: suite.testAccounts["admin_account"].ID,
|
||||
PrivateComment: "this domain is poo",
|
||||
}
|
||||
)
|
||||
|
||||
// Whack the exclude in.
|
||||
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Get the exclude again.
|
||||
dbExclude, err := suite.state.DB.GetDomainPermissionExcludeByID(ctx, exclude.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Domain should have been stored punycoded.
|
||||
suite.Equal("xn--exmple-cua.org", dbExclude.Domain)
|
||||
|
||||
// Search for domain using both
|
||||
// punycode and unicode variants.
|
||||
search1, err := suite.state.DB.GetDomainPermissionExcludes(
|
||||
ctx,
|
||||
"exämple.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search1) != 1 {
|
||||
suite.FailNow("couldn't get domain perm exclude exämple.org")
|
||||
}
|
||||
|
||||
search2, err := suite.state.DB.GetDomainPermissionExcludes(
|
||||
ctx,
|
||||
"xn--exmple-cua.org",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if len(search2) != 1 {
|
||||
suite.FailNow("couldn't get domain perm exclude example.org")
|
||||
}
|
||||
|
||||
// Change ID + try to put the same exclude again.
|
||||
exclude.ID = "01JCZNVYSDT3JE385FABMJ7ADQ"
|
||||
err = suite.state.DB.PutDomainPermissionExclude(ctx, exclude)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("was able to insert same domain perm exclude twice")
|
||||
}
|
||||
|
||||
// Delete both excludes.
|
||||
for _, id := range []string{
|
||||
"01JCZN614XG85GCGAMSV9ZZAEJ",
|
||||
"01JCZNVYSDT3JE385FABMJ7ADQ",
|
||||
} {
|
||||
if err := suite.state.DB.DeleteDomainPermissionExclude(ctx, id); err != nil {
|
||||
suite.FailNow("error deleting domain permission exclude")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DomainPermissionExcludeTestSuite) TestExcluded() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
createdByAccountID = suite.testAccounts["admin_account"].ID
|
||||
)
|
||||
|
||||
// Insert some excludes into the db.
|
||||
for _, exclude := range []*gtsmodel.DomainPermissionExclude{
|
||||
{
|
||||
ID: "01JD7AFFBBZSPY8R2M0JCGQGPW",
|
||||
Domain: "example.org",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AMK98E2QX78KXEZJ1RF5Z",
|
||||
Domain: "boobs.com",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AMXW3R3W98E91R62ACDA0",
|
||||
Domain: "rad.boobs.com",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
{
|
||||
ID: "01JD7AYYN5TXQVASB30PT08CE1",
|
||||
Domain: "honkers.org",
|
||||
CreatedByAccountID: createdByAccountID,
|
||||
},
|
||||
} {
|
||||
if err := suite.state.DB.PutDomainPermissionExclude(ctx, exclude); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
domain string
|
||||
excluded bool
|
||||
}
|
||||
|
||||
for i, testCase := range []testCase{
|
||||
{
|
||||
domain: config.GetHost(),
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "test.example.org",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "example.org",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "boobs.com",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "rad.boobs.com",
|
||||
excluded: true,
|
||||
},
|
||||
{
|
||||
domain: "sir.not.appearing.in.this.list",
|
||||
excluded: false,
|
||||
},
|
||||
} {
|
||||
excluded, err := suite.state.DB.IsDomainPermissionExcluded(ctx, testCase.domain)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
if excluded != testCase.excluded {
|
||||
suite.Failf("",
|
||||
"test %d: %s excluded should be %t",
|
||||
i, testCase.domain, testCase.excluded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainPermissionExcludeTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DomainPermissionExcludeTestSuite))
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
|
@ -597,7 +597,7 @@ func (e *emojiDB) GetEmojisByIDs(ctx context.Context, ids []string) ([]*gtsmodel
|
|||
// Reorder the emojis by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(e *gtsmodel.Emoji) string { return e.ID }
|
||||
util.OrderBy(emojis, ids, getID)
|
||||
xslices.OrderBy(emojis, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -661,7 +661,7 @@ func (e *emojiDB) GetEmojiCategoriesByIDs(ctx context.Context, ids []string) ([]
|
|||
// Reorder the categories by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(c *gtsmodel.EmojiCategory) string { return c.ID }
|
||||
util.OrderBy(categories, ids, getID)
|
||||
xslices.OrderBy(categories, ids, getID)
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func (f *filterDB) GetFiltersForAccountID(ctx context.Context, accountID string)
|
|||
}
|
||||
|
||||
// Put the filter structs in the same order as the filter IDs.
|
||||
util.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
|
||||
xslices.OrderBy(filters, filterIDs, func(filter *gtsmodel.Filter) string { return filter.ID })
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
return filters, nil
|
||||
|
|
|
|||
|
|
@ -247,6 +247,54 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
|
|||
}
|
||||
}
|
||||
|
||||
func (suite *FilterTestSuite) TestFilterTitleOverlap() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
account1 = "01HNEJXCPRTJVJY9MV0VVHGD47"
|
||||
account2 = "01JAG5BRJPJYA0FSA5HR2MMFJH"
|
||||
)
|
||||
|
||||
// Create an empty filter for account 1.
|
||||
account1filter1 := >smodel.Filter{
|
||||
ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
|
||||
AccountID: account1,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
if err := suite.db.PutFilter(ctx, account1filter1); err != nil {
|
||||
suite.FailNow("", "error putting account1filter1: %s", err)
|
||||
}
|
||||
|
||||
// Create a filter for account 2 with
|
||||
// the same title, should be no issue.
|
||||
account2filter1 := >smodel.Filter{
|
||||
ID: "01JAG5GPXG7H5Y4ZP78GV1F2ET",
|
||||
AccountID: account2,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
if err := suite.db.PutFilter(ctx, account2filter1); err != nil {
|
||||
suite.FailNow("", "error putting account2filter1: %s", err)
|
||||
}
|
||||
|
||||
// Try to create another filter for
|
||||
// account 1 with the same name as
|
||||
// an existing filter of theirs.
|
||||
account1filter2 := >smodel.Filter{
|
||||
ID: "01JAG5J8NYKQE2KYCD28Y4P05V",
|
||||
AccountID: account1,
|
||||
Title: "my filter",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
}
|
||||
err := suite.db.PutFilter(ctx, account1filter2)
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
suite.FailNow("", "wanted ErrAlreadyExists, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(FilterTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ func (f *filterDB) getFilterKeywords(ctx context.Context, idColumn string, id st
|
|||
}
|
||||
|
||||
// Put the filter keyword structs in the same order as the filter keyword IDs.
|
||||
util.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
|
||||
xslices.OrderBy(filterKeywords, filterKeywordIDs, func(filterKeyword *gtsmodel.FilterKeyword) string {
|
||||
return filterKeyword.ID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ func (f *filterDB) getFilterStatuses(ctx context.Context, idColumn string, id st
|
|||
}
|
||||
|
||||
// Put the filter status structs in the same order as the filter status IDs.
|
||||
util.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
|
||||
xslices.OrderBy(filterStatuses, filterStatusIDs, func(filterStatus *gtsmodel.FilterStatus) string {
|
||||
return filterStatus.ID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
|
|||
// Ignore statuses that are currently pending approval.
|
||||
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
|
||||
|
||||
// Ignore statuses that are direct messages.
|
||||
q = q.Where("NOT ? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityDirect)
|
||||
|
||||
count, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
|
|||
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
|
||||
suite.NoError(err)
|
||||
suite.Equal(20, count)
|
||||
suite.Equal(19, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -84,6 +86,53 @@ func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri stri
|
|||
)
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionRequestsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Load all interaction request IDs via cache loader callbacks.
|
||||
requests, err := i.state.Caches.DB.InteractionRequest.LoadIDs("ID",
|
||||
ids,
|
||||
func(uncached []string) ([]*gtsmodel.InteractionRequest, error) {
|
||||
// Preallocate expected length of uncached interaction requests.
|
||||
requests := make([]*gtsmodel.InteractionRequest, 0, len(uncached))
|
||||
|
||||
// Perform database query scanning
|
||||
// the remaining (uncached) IDs.
|
||||
if err := i.db.NewSelect().
|
||||
Model(&requests).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(uncached)).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return requests, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reorder the requests by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(r *gtsmodel.InteractionRequest) string { return r.ID }
|
||||
xslices.OrderBy(requests, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
// Populate all loaded interaction requests, removing those we
|
||||
// fail to populate (removes needing so many nil checks everywhere).
|
||||
requests = slices.DeleteFunc(requests, func(request *gtsmodel.InteractionRequest) bool {
|
||||
if err := i.PopulateInteractionRequest(ctx, request); err != nil {
|
||||
log.Errorf(ctx, "error populating %s: %v", request.ID, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) getInteractionRequest(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
|
|
@ -205,13 +254,18 @@ func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *g
|
|||
}
|
||||
|
||||
func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
|
||||
defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
// Delete interaction request by ID.
|
||||
if _, err := i.db.NewDelete().
|
||||
Table("interaction_requests").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := i.db.NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("interaction_requests"), bun.Ident("interaction_request")).
|
||||
Where("? = ?", bun.Ident("interaction_request.id"), id).
|
||||
Exec(ctx)
|
||||
return err
|
||||
// Invalidate cached interaction request with ID.
|
||||
i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *interactionDB) GetInteractionsRequestsForAcct(
|
||||
|
|
@ -248,9 +302,9 @@ func (i *interactionDB) GetInteractionsRequestsForAcct(
|
|||
bun.Ident("interaction_request"),
|
||||
).
|
||||
// Select only interaction requests that
|
||||
// are neither accepted or rejected yet,
|
||||
// ie., without an Accept or Reject URI.
|
||||
Where("? IS NULL", bun.Ident("uri"))
|
||||
// are neither accepted or rejected yet.
|
||||
Where("? IS NULL", bun.Ident("accepted_at")).
|
||||
Where("? IS NULL", bun.Ident("rejected_at"))
|
||||
|
||||
// Select interactions targeting status.
|
||||
if statusID != "" {
|
||||
|
|
@ -317,19 +371,8 @@ func (i *interactionDB) GetInteractionsRequestsForAcct(
|
|||
slices.Reverse(reqIDs)
|
||||
}
|
||||
|
||||
// For each interaction request ID,
|
||||
// select the interaction request.
|
||||
reqs := make([]*gtsmodel.InteractionRequest, 0, len(reqIDs))
|
||||
for _, id := range reqIDs {
|
||||
req, err := i.GetInteractionRequestByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
|
||||
return reqs, nil
|
||||
// Load all interaction requests by their IDs.
|
||||
return i.GetInteractionRequestsByIDs(ctx, reqIDs)
|
||||
}
|
||||
|
||||
func (i *interactionDB) IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -333,7 +333,7 @@ func (l *listDB) GetListsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.L
|
|||
// Reorder the lists by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(l *gtsmodel.List) string { return l.ID }
|
||||
util.OrderBy(lists, ids, getID)
|
||||
xslices.OrderBy(lists, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -387,12 +387,12 @@ func (l *listDB) PutListEntries(ctx context.Context, entries []*gtsmodel.ListEnt
|
|||
}
|
||||
|
||||
// Collect unique list IDs from the provided list entries.
|
||||
listIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
listIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
return e.ListID
|
||||
})
|
||||
|
||||
// Collect unique follow IDs from the provided list entries.
|
||||
followIDs := util.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
followIDs := xslices.Collate(entries, func(e *gtsmodel.ListEntry) string {
|
||||
return e.FollowID
|
||||
})
|
||||
|
||||
|
|
@ -441,7 +441,7 @@ func (l *listDB) DeleteAllListEntriesByFollows(ctx context.Context, followIDs ..
|
|||
}
|
||||
|
||||
// Deduplicate IDs before invalidate.
|
||||
listIDs = util.Deduplicate(listIDs)
|
||||
listIDs = xslices.Deduplicate(listIDs)
|
||||
|
||||
// Invalidate all related list entry caches.
|
||||
l.invalidateEntryCaches(ctx, listIDs, followIDs)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ func (m *mediaDB) GetAttachmentsByIDs(ctx context.Context, ids []string) ([]*gts
|
|||
// Reorder the media by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(m *gtsmodel.MediaAttachment) string { return m.ID }
|
||||
util.OrderBy(media, ids, getID)
|
||||
xslices.OrderBy(media, ids, getID)
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func (m *mentionDB) GetMentions(ctx context.Context, ids []string) ([]*gtsmodel.
|
|||
// Reorder the mentions by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(m *gtsmodel.Mention) string { return m.ID }
|
||||
util.OrderBy(mentions, ids, getID)
|
||||
xslices.OrderBy(mentions, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func init() {
|
|||
UpdatedAt: oldAction.UpdatedAt,
|
||||
TargetCategory: gtsmodel.AdminActionCategoryAccount,
|
||||
TargetID: oldAction.TargetAccountID,
|
||||
Type: gtsmodel.NewAdminActionType(string(oldAction.Type)),
|
||||
Type: gtsmodel.ParseAdminActionType(string(oldAction.Type)),
|
||||
AccountID: oldAction.AccountID,
|
||||
Text: oldAction.Text,
|
||||
SendEmail: util.Ptr(oldAction.SendEmail),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import (
|
|||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20231002153327_add_status_polls"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Poll represents an attached (to) Status poll, i.e. a questionaire. Can be remote / local.
|
||||
type Poll struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string.
|
||||
Multiple *bool `bun:",nullzero,notnull,default:false"` // Is this a multiple choice poll? i.e. can you vote on multiple options.
|
||||
HideCounts *bool `bun:",nullzero,notnull,default:false"` // Hides vote counts until poll ends.
|
||||
Options []string `bun:",nullzero,notnull"` // The available options for this poll.
|
||||
Votes []int `bun:",nullzero,notnull"` // Vote counts per choice.
|
||||
Voters *int `bun:",nullzero,notnull"` // Total no. voters count.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull,unique"` // Status ID of which this Poll is attached to.
|
||||
ExpiresAt time.Time `bun:"type:timestamptz,nullzero,notnull"` // The expiry date of this Poll.
|
||||
ClosedAt time.Time `bun:"type:timestamptz,nullzero"` // The closure date of this poll, will be zerotime until set.
|
||||
Closing bool `bun:"-"` // An ephemeral field only set on Polls in the middle of closing.
|
||||
// no creation date, use attached Status.CreatedAt.
|
||||
}
|
||||
|
||||
// PollVote represents a single instance of vote(s) in a Poll by an account.
|
||||
// If the Poll is single-choice, len(.Choices) = 1, if multiple-choice then
|
||||
// len(.Choices) >= 1. Can be remote or local.
|
||||
type PollVote struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // Unique identity string.
|
||||
Choices []int `bun:",nullzero,notnull"` // The Poll's option indices of which these are votes for.
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Account ID from which this vote originated.
|
||||
PollID string `bun:"type:CHAR(26),nullzero,notnull,unique:in_poll_by_account"` // Poll ID of which this is a vote in.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation date of this PollVote.
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged
|
||||
Replyable *bool `bun:",notnull"` // This status can be replied to
|
||||
Likeable *bool `bun:",notnull"` // This status can be liked/faved
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -20,7 +20,7 @@ package migrations
|
|||
import (
|
||||
"context"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240126064004_add_filters"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filter stores a filter created by a local account.
|
||||
type Filter struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
ExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Time filter should expire. If null, should not expire.
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter.
|
||||
Title string `bun:",nullzero,notnull,unique"` // The name of the filter.
|
||||
Action string `bun:",nullzero,notnull"` // The action to take.
|
||||
Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter.
|
||||
Statuses []*FilterStatus `bun:"-"` // Statuses for this filter.
|
||||
ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
|
||||
ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications.
|
||||
ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
|
||||
ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread.
|
||||
ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile.
|
||||
}
|
||||
|
||||
// FilterKeyword stores a single keyword to filter statuses against.
|
||||
type FilterKeyword struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
|
||||
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_keywords_filter_id_keyword_uniq"` // ID of the filter that this keyword belongs to.
|
||||
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
|
||||
Keyword string `bun:",nullzero,notnull,unique:filter_keywords_filter_id_keyword_uniq"` // The keyword or phrase to filter against.
|
||||
WholeWord *bool `bun:",nullzero,notnull,default:false"` // Should the filter consider word boundaries?
|
||||
Regexp *regexp.Regexp `bun:"-"` // pre-prepared regular expression
|
||||
}
|
||||
|
||||
// FilterStatus stores a single status to filter.
|
||||
type FilterStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
|
||||
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the filter that this keyword belongs to.
|
||||
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
|
||||
StatusID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the status to filter.
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ func init() {
|
|||
table string
|
||||
column string
|
||||
columnType string
|
||||
defaultVal string
|
||||
extra string
|
||||
}
|
||||
for _, spec := range []spec{
|
||||
// Statuses.
|
||||
|
|
@ -48,19 +48,19 @@ func init() {
|
|||
table: "statuses",
|
||||
column: "interaction_policy",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "statuses",
|
||||
column: "pending_approval",
|
||||
columnType: "BOOLEAN",
|
||||
defaultVal: "DEFAULT false",
|
||||
extra: "NOT NULL DEFAULT false",
|
||||
},
|
||||
{
|
||||
table: "statuses",
|
||||
column: "approved_by_uri",
|
||||
columnType: "varchar",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
|
||||
// Status faves.
|
||||
|
|
@ -68,13 +68,13 @@ func init() {
|
|||
table: "status_faves",
|
||||
column: "pending_approval",
|
||||
columnType: "BOOLEAN",
|
||||
defaultVal: "DEFAULT false",
|
||||
extra: "NOT NULL DEFAULT false",
|
||||
},
|
||||
{
|
||||
table: "status_faves",
|
||||
column: "approved_by_uri",
|
||||
columnType: "varchar",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
|
||||
// Columns that must be added to the
|
||||
|
|
@ -85,31 +85,31 @@ func init() {
|
|||
table: "account_settings",
|
||||
column: "interaction_policy_direct",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_mutuals_only",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_followers_only",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_unlocked",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
{
|
||||
table: "account_settings",
|
||||
column: "interaction_policy_public",
|
||||
columnType: "JSONB",
|
||||
defaultVal: "",
|
||||
extra: "",
|
||||
},
|
||||
} {
|
||||
exists, err := doesColumnExist(ctx, tx,
|
||||
|
|
@ -130,9 +130,9 @@ func init() {
|
|||
}
|
||||
|
||||
qStr := "ALTER TABLE ? ADD COLUMN ? ?"
|
||||
if spec.defaultVal != "" {
|
||||
if spec.extra != "" {
|
||||
qStr += " ?"
|
||||
args = append(args, bun.Safe(spec.defaultVal))
|
||||
args = append(args, bun.Safe(spec.extra))
|
||||
}
|
||||
|
||||
log.Infof(ctx, "adding column '%s' to '%s'...", spec.column, spec.table)
|
||||
|
|
@ -161,11 +161,14 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to new integer values.
|
||||
visibilityMapping := visibilityEnumMapping[oldmodel.Visibility]()
|
||||
|
||||
// For each status found in this way, update
|
||||
// to new version of interaction policy.
|
||||
for _, oldStatus := range oldStatuses {
|
||||
// Start with default policy for this visibility.
|
||||
v := gtsmodel.Visibility(oldStatus.Visibility)
|
||||
v := visibilityMapping[oldStatus.Visibility]
|
||||
policy := gtsmodel.DefaultInteractionPolicyFor(v)
|
||||
|
||||
if !*oldStatus.Likeable {
|
||||
|
|
|
|||
|
|
@ -22,40 +22,61 @@ import (
|
|||
)
|
||||
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
URI string `bun:",unique,nullzero,notnull"`
|
||||
URL string `bun:",nullzero"`
|
||||
Content string `bun:""`
|
||||
AttachmentIDs []string `bun:"attachments,array"`
|
||||
TagIDs []string `bun:"tags,array"`
|
||||
MentionIDs []string `bun:"mentions,array"`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Local *bool `bun:",nullzero,notnull,default:false"`
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
AccountURI string `bun:",nullzero,notnull"`
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyToURI string `bun:",nullzero"`
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyTo *Status `bun:"-"`
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOfURI string `bun:"-"`
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOf *Status `bun:"-"`
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"`
|
||||
PollID string `bun:"type:CHAR(26),nullzero"`
|
||||
ContentWarning string `bun:",nullzero"`
|
||||
Visibility string `bun:",nullzero,notnull"`
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"`
|
||||
Language string `bun:",nullzero"`
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"`
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"`
|
||||
Text string `bun:""`
|
||||
Federated *bool `bun:",notnull"`
|
||||
Boostable *bool `bun:",notnull"`
|
||||
Replyable *bool `bun:",notnull"`
|
||||
Likeable *bool `bun:",notnull"`
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
URI string `bun:",unique,nullzero,notnull"`
|
||||
URL string `bun:",nullzero"`
|
||||
Content string `bun:""`
|
||||
AttachmentIDs []string `bun:"attachments,array"`
|
||||
TagIDs []string `bun:"tags,array"`
|
||||
MentionIDs []string `bun:"mentions,array"`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Local *bool `bun:",nullzero,notnull,default:false"`
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
AccountURI string `bun:",nullzero,notnull"`
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyToURI string `bun:",nullzero"`
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
InReplyTo *Status `bun:"-"`
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOfURI string `bun:"-"`
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"`
|
||||
BoostOf *Status `bun:"-"`
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"`
|
||||
PollID string `bun:"type:CHAR(26),nullzero"`
|
||||
ContentWarning string `bun:",nullzero"`
|
||||
Visibility Visibility `bun:",nullzero,notnull"`
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"`
|
||||
Language string `bun:",nullzero"`
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"`
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"`
|
||||
Text string `bun:""`
|
||||
Federated *bool `bun:",notnull"`
|
||||
Boostable *bool `bun:",notnull"`
|
||||
Replyable *bool `bun:",notnull"`
|
||||
Likeable *bool `bun:",notnull"`
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ package migrations
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240904084406_fedi_api_reject_interaction"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// SinBinStatus represents a status that's been rejected and/or reported + quarantined.
|
||||
//
|
||||
// Automatically rejected statuses are not put in the sin bin, only statuses that were
|
||||
// stored on the instance and which someone (local or remote) has subsequently rejected.
|
||||
type SinBinStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item.
|
||||
URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status.
|
||||
URL string `bun:",nullzero"` // Web url for viewing this status.
|
||||
Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`.
|
||||
AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status.
|
||||
InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to.
|
||||
Content string `bun:",nullzero"` // Content of this status.
|
||||
AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status.
|
||||
MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts.
|
||||
EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status.
|
||||
PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status.
|
||||
ContentWarning string `bun:",nullzero"` // CW / subject string for this status.
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive.
|
||||
Language string `bun:",nullzero"` // Language code for this status.
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
for idx, col := range map[string]string{
|
||||
"interaction_requests_accepted_at_idx": "accepted_at",
|
||||
"interaction_requests_rejected_at_idx": "rejected_at",
|
||||
} {
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("interaction_requests").
|
||||
Index(idx).
|
||||
Column(col).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
// Previous versions of 20240620074530_interaction_policy.go
|
||||
// didn't set NOT NULL on gtsmodel.Status.PendingApproval and
|
||||
// gtsmodel.StatusFave.PendingApproval, resulting in NULL being
|
||||
// set for that column for some statuses. Correct for this.
|
||||
|
||||
log.Info(ctx, "correcting pending_approval on statuses table...")
|
||||
res, err := tx.
|
||||
NewUpdate().
|
||||
Table("statuses").
|
||||
Set("? = ?", bun.Ident("pending_approval"), false).
|
||||
Where("? IS NULL", bun.Ident("pending_approval")).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := res.RowsAffected()
|
||||
if err == nil {
|
||||
log.Infof(ctx, "corrected %d entries", rows)
|
||||
}
|
||||
|
||||
log.Info(ctx, "correcting pending_approval on status_faves table...")
|
||||
res, err = tx.
|
||||
NewUpdate().
|
||||
Table("status_faves").
|
||||
Set("? = ?", bun.Ident("pending_approval"), false).
|
||||
Where("? IS NULL", bun.Ident("pending_approval")).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err = res.RowsAffected()
|
||||
if err == nil {
|
||||
log.Infof(ctx, "corrected %d entries", rows)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
131
internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
Normal file
131
internal/db/bundb/migrations/20241018151036_filter_unique_fix.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Create the new filters table
|
||||
// with the unique constraint
|
||||
// set on AccountID + Title.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
ModelTableExpr("new_filters").
|
||||
Model((*gtsmodel.Filter)(nil)).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Explicitly specify columns to bring
|
||||
// from old table to new, to avoid any
|
||||
// potential Postgres shenanigans.
|
||||
columns := []string{
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"expires_at",
|
||||
"account_id",
|
||||
"title",
|
||||
"action",
|
||||
"context_home",
|
||||
"context_notifications",
|
||||
"context_public",
|
||||
"context_thread",
|
||||
"context_account",
|
||||
}
|
||||
|
||||
// Copy all data for existing
|
||||
// filters to the new table.
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
Table("new_filters").
|
||||
Table("filters").
|
||||
Column(columns...).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop the old table.
|
||||
if _, err := tx.
|
||||
NewDropTable().
|
||||
Table("filters").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename new table to old table.
|
||||
if _, err := tx.
|
||||
ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? RENAME TO ?",
|
||||
bun.Ident("new_filters"),
|
||||
bun.Ident("filters"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Index the new version
|
||||
// of the filters table.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("filters").
|
||||
Index("filters_account_id_idx").
|
||||
Column("account_id").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if db.Dialect().Name() == dialect.PG {
|
||||
// Rename "new_filters_pkey" from the
|
||||
// new table to just "filters_pkey".
|
||||
// This is only necessary on Postgres.
|
||||
if _, err := tx.ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? RENAME CONSTRAINT ? TO ?",
|
||||
bun.Ident("public.filters"),
|
||||
bun.Safe("new_filters_pkey"),
|
||||
bun.Safe("filters_pkey"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Create `domain_permission_drafts`.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model((*gtsmodel.DomainPermissionDraft)(nil)).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create `domain_permission_ignores`.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model((*gtsmodel.DomainPermissionExclude)(nil)).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create indexes. Indices. Indie sexes.
|
||||
for table, indexes := range map[string]map[string][]string{
|
||||
"domain_permission_drafts": {
|
||||
"domain_permission_drafts_domain_idx": {"domain"},
|
||||
"domain_permission_drafts_subscription_id_idx": {"subscription_id"},
|
||||
},
|
||||
} {
|
||||
for index, columns := range indexes {
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table(table).
|
||||
Index(index).
|
||||
Column(columns...).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Tables with visibility types.
|
||||
var visTables = []struct {
|
||||
Table string
|
||||
Column string
|
||||
Default *new_gtsmodel.Visibility
|
||||
}{
|
||||
{Table: "statuses", Column: "visibility"},
|
||||
{Table: "sin_bin_statuses", Column: "visibility"},
|
||||
{Table: "account_settings", Column: "privacy", Default: util.Ptr(new_gtsmodel.VisibilityDefault)},
|
||||
{Table: "account_settings", Column: "web_visibility", Default: util.Ptr(new_gtsmodel.VisibilityDefault)},
|
||||
}
|
||||
|
||||
// Visibility type indices.
|
||||
var visIndices = []struct {
|
||||
name string
|
||||
cols []string
|
||||
order string
|
||||
}{
|
||||
{
|
||||
name: "statuses_visibility_idx",
|
||||
cols: []string{"visibility"},
|
||||
order: "",
|
||||
},
|
||||
{
|
||||
name: "statuses_profile_web_view_idx",
|
||||
cols: []string{"account_id", "visibility"},
|
||||
order: "id DESC",
|
||||
},
|
||||
{
|
||||
name: "statuses_public_timeline_idx",
|
||||
cols: []string{"visibility"},
|
||||
order: "id DESC",
|
||||
},
|
||||
}
|
||||
|
||||
// Before making changes to the visibility col
|
||||
// we must drop all indices that rely on it.
|
||||
for _, index := range visIndices {
|
||||
if _, err := tx.NewDropIndex().
|
||||
Index(index.name).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to new integer values.
|
||||
visibilityMapping := visibilityEnumMapping[old_gtsmodel.Visibility]()
|
||||
|
||||
// Convert all visibility tables.
|
||||
for _, table := range visTables {
|
||||
if err := convertEnums(ctx, tx, table.Table, table.Column,
|
||||
visibilityMapping, table.Default); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the visibility indices.
|
||||
for _, index := range visIndices {
|
||||
q := tx.NewCreateIndex().
|
||||
Table("statuses").
|
||||
Index(index.name).
|
||||
Column(index.cols...)
|
||||
if index.order != "" {
|
||||
q = q.ColumnExpr(index.order)
|
||||
}
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the mapping of old enum string values to the new integer value types.
|
||||
notificationMapping := notificationEnumMapping[old_gtsmodel.NotificationType]()
|
||||
|
||||
// Migrate over old notifications table column over to new column type.
|
||||
if err := convertEnums(ctx, tx, "notifications", "notification_type", //nolint:revive
|
||||
notificationMapping, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// convertEnums performs a transaction that converts
|
||||
// a table's column of our old-style enums (strings) to
|
||||
// more performant and space-saving integer types.
|
||||
func convertEnums[OldType ~string, NewType ~int16](
|
||||
ctx context.Context,
|
||||
tx bun.Tx,
|
||||
table string,
|
||||
column string,
|
||||
mapping map[OldType]NewType,
|
||||
defaultValue *NewType,
|
||||
) error {
|
||||
if len(mapping) == 0 {
|
||||
return errors.New("empty mapping")
|
||||
}
|
||||
|
||||
// Generate new column name.
|
||||
newColumn := column + "_new"
|
||||
|
||||
log.Infof(ctx, "converting %s.%s enums; "+
|
||||
"this may take a while, please don't interrupt!",
|
||||
table, column,
|
||||
)
|
||||
|
||||
// Ensure a default value.
|
||||
if defaultValue == nil {
|
||||
var zero NewType
|
||||
defaultValue = &zero
|
||||
}
|
||||
|
||||
// Add new column to database.
|
||||
if _, err := tx.NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
|
||||
bun.Ident(newColumn),
|
||||
*defaultValue).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error adding new column: %w", err)
|
||||
}
|
||||
|
||||
// Get a count of all in table.
|
||||
total, err := tx.NewSelect().
|
||||
Table(table).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error selecting total count: %w", err)
|
||||
}
|
||||
|
||||
var updated int
|
||||
for old, new := range mapping {
|
||||
|
||||
// Update old to new values.
|
||||
res, err := tx.NewUpdate().
|
||||
Table(table).
|
||||
Where("? = ?", bun.Ident(column), old).
|
||||
Set("? = ?", bun.Ident(newColumn), new).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error updating old column values: %w", err)
|
||||
}
|
||||
|
||||
// Count number items updated.
|
||||
n, _ := res.RowsAffected()
|
||||
updated += int(n)
|
||||
}
|
||||
|
||||
// Check total updated.
|
||||
if total != updated {
|
||||
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
|
||||
}
|
||||
|
||||
// Drop the old column from table.
|
||||
if _, err := tx.NewDropColumn().
|
||||
Table(table).
|
||||
ColumnExpr("?", bun.Ident(column)).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error dropping old column: %w", err)
|
||||
}
|
||||
|
||||
// Rename new to old name.
|
||||
if _, err := tx.NewRaw(
|
||||
"ALTER TABLE ? RENAME COLUMN ? TO ?",
|
||||
bun.Ident(table),
|
||||
bun.Ident(newColumn),
|
||||
bun.Ident(column),
|
||||
).Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error renaming new column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// visibilityEnumMapping maps old Visibility enum values to their newer integer type.
|
||||
func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility {
|
||||
return map[T]new_gtsmodel.Visibility{
|
||||
T(old_gtsmodel.VisibilityNone): new_gtsmodel.VisibilityNone,
|
||||
T(old_gtsmodel.VisibilityPublic): new_gtsmodel.VisibilityPublic,
|
||||
T(old_gtsmodel.VisibilityUnlocked): new_gtsmodel.VisibilityUnlocked,
|
||||
T(old_gtsmodel.VisibilityFollowersOnly): new_gtsmodel.VisibilityFollowersOnly,
|
||||
T(old_gtsmodel.VisibilityMutualsOnly): new_gtsmodel.VisibilityMutualsOnly,
|
||||
T(old_gtsmodel.VisibilityDirect): new_gtsmodel.VisibilityDirect,
|
||||
}
|
||||
}
|
||||
|
||||
// notificationEnumMapping maps old NotificationType enum values to their newer integer type.
|
||||
func notificationEnumMapping[T ~string]() map[T]new_gtsmodel.NotificationType {
|
||||
return map[T]new_gtsmodel.NotificationType{
|
||||
T(old_gtsmodel.NotificationFollow): new_gtsmodel.NotificationFollow,
|
||||
T(old_gtsmodel.NotificationFollowRequest): new_gtsmodel.NotificationFollowRequest,
|
||||
T(old_gtsmodel.NotificationMention): new_gtsmodel.NotificationMention,
|
||||
T(old_gtsmodel.NotificationReblog): new_gtsmodel.NotificationReblog,
|
||||
T(old_gtsmodel.NotificationFave): new_gtsmodel.NotificationFave,
|
||||
T(old_gtsmodel.NotificationPoll): new_gtsmodel.NotificationPoll,
|
||||
T(old_gtsmodel.NotificationStatus): new_gtsmodel.NotificationStatus,
|
||||
T(old_gtsmodel.NotificationSignup): new_gtsmodel.NotificationSignup,
|
||||
T(old_gtsmodel.NotificationPendingFave): new_gtsmodel.NotificationPendingFave,
|
||||
T(old_gtsmodel.NotificationPendingReply): new_gtsmodel.NotificationPendingReply,
|
||||
T(old_gtsmodel.NotificationPendingReblog): new_gtsmodel.NotificationPendingReblog,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// AccountSettings models settings / preferences for a local, non-instance account.
|
||||
type AccountSettings struct {
|
||||
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
|
||||
Privacy Visibility `bun:",nullzero,default:3"` // Default post privacy for this account
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
|
||||
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
|
||||
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
|
||||
Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
|
||||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile.
|
||||
InteractionPolicyDirect *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
||||
InteractionPolicyMutualsOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyFollowersOnly *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyUnlocked *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyPublic *gtsmodel.InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy.
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc.
|
||||
type Notification struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
NotificationType NotificationType `bun:",nullzero,notnull"` // Type of this notification
|
||||
TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account targeted by the notification (ie., who will receive the notification?)
|
||||
TargetAccount *gtsmodel.Account `bun:"-"` // Account corresponding to TargetAccountID. Can be nil, always check first + select using ID if necessary.
|
||||
OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account that performed the action that created the notification.
|
||||
OriginAccount *gtsmodel.Account `bun:"-"` // Account corresponding to OriginAccountID. Can be nil, always check first + select using ID if necessary.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero"` // If the notification pertains to a status, what is the database ID of that status?
|
||||
Status *Status `bun:"-"` // Status corresponding to StatusID. Can be nil, always check first + select using ID if necessary.
|
||||
Read *bool `bun:",nullzero,notnull,default:false"` // Notification has been seen/read
|
||||
}
|
||||
|
||||
// NotificationType describes the reason/type of this notification.
|
||||
type NotificationType string
|
||||
|
||||
// Notification Types
|
||||
const (
|
||||
NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you
|
||||
NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you
|
||||
NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status
|
||||
NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses
|
||||
NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses
|
||||
NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended
|
||||
NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status.
|
||||
NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance.
|
||||
NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you.
|
||||
NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you.
|
||||
NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you.
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// SinBinStatus represents a status that's been rejected and/or reported + quarantined.
|
||||
//
|
||||
// Automatically rejected statuses are not put in the sin bin, only statuses that were
|
||||
// stored on the instance and which someone (local or remote) has subsequently rejected.
|
||||
type SinBinStatus struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last-updated time of this item.
|
||||
URI string `bun:",unique,nullzero,notnull"` // ActivityPub URI/ID of this status.
|
||||
URL string `bun:",nullzero"` // Web url for viewing this status.
|
||||
Domain string `bun:",nullzero"` // Domain of the status, will be null if this is a local status, otherwise something like `example.org`.
|
||||
AccountURI string `bun:",nullzero,notnull"` // ActivityPub uri of the author of this status.
|
||||
InReplyToURI string `bun:",nullzero"` // ActivityPub uri of the status this status is a reply to.
|
||||
Content string `bun:",nullzero"` // Content of this status.
|
||||
AttachmentLinks []string `bun:",nullzero,array"` // Links to attachments of this status.
|
||||
MentionTargetURIs []string `bun:",nullzero,array"` // URIs of mentioned accounts.
|
||||
EmojiLinks []string `bun:",nullzero,array"` // Links to any emoji images used in this status.
|
||||
PollOptions []string `bun:",nullzero,array"` // String values of any poll options used in this status.
|
||||
ContentWarning string `bun:",nullzero"` // CW / subject string for this status.
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // Visibility level of this status.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Mark the status as sensitive.
|
||||
Language string `bun:",nullzero"` // Language code for this status.
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // ActivityStreams type of this status.
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// 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 gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
type Status struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
PinnedAt time.Time `bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time.
|
||||
URI string `bun:",unique,nullzero,notnull"` // activitypub URI of this status
|
||||
URL string `bun:",nullzero"` // web url for viewing this status
|
||||
Content string `bun:""` // content of this status; likely html-formatted but not guaranteed
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of any media attachments associated with this status
|
||||
Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
Local *bool `bun:",nullzero,notnull,default:false"` // is this status from a local account?
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status?
|
||||
Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID
|
||||
AccountURI string `bun:",nullzero,notnull"` // activitypub uri of the owner of this status
|
||||
InReplyToID string `bun:"type:CHAR(26),nullzero"` // id of the status this status replies to
|
||||
InReplyToURI string `bun:",nullzero"` // activitypub uri of the status this status is a reply to
|
||||
InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to
|
||||
InReplyTo *Status `bun:"-"` // status corresponding to inReplyToID
|
||||
InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID
|
||||
BoostOfID string `bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of
|
||||
BoostOfURI string `bun:"-"` // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes.
|
||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||
BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
||||
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
Poll *gtsmodel.Poll `bun:"-"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||
Language string `bun:",nullzero"` // what language is this status written in?
|
||||
CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application was used to create this status?
|
||||
CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID
|
||||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
InteractionPolicy *gtsmodel.InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -26,10 +26,10 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ func (n *notificationDB) GetNotificationsByIDs(ctx context.Context, ids []string
|
|||
// Reorder the notifs by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(n *gtsmodel.Notification) string { return n.ID }
|
||||
util.OrderBy(notifs, ids, getID)
|
||||
xslices.OrderBy(notifs, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -192,22 +192,19 @@ func (n *notificationDB) PopulateNotification(ctx context.Context, notif *gtsmod
|
|||
func (n *notificationDB) GetAccountNotifications(
|
||||
ctx context.Context,
|
||||
accountID string,
|
||||
maxID string,
|
||||
sinceID string,
|
||||
minID string,
|
||||
limit int,
|
||||
types []string,
|
||||
excludeTypes []string,
|
||||
page *paging.Page,
|
||||
types []gtsmodel.NotificationType,
|
||||
excludeTypes []gtsmodel.NotificationType,
|
||||
) ([]*gtsmodel.Notification, error) {
|
||||
// Ensure reasonable
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Make educated guess for slice size
|
||||
var (
|
||||
notifIDs = make([]string, 0, limit)
|
||||
frontToBack = true
|
||||
// Get paging params.
|
||||
minID = page.GetMin()
|
||||
maxID = page.GetMax()
|
||||
limit = page.GetLimit()
|
||||
order = page.GetOrder()
|
||||
|
||||
// Make educated guess for slice size
|
||||
notifIDs = make([]string, 0, limit)
|
||||
)
|
||||
|
||||
q := n.db.
|
||||
|
|
@ -215,23 +212,14 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Column("notification.id")
|
||||
|
||||
if maxID == "" {
|
||||
maxID = id.Highest
|
||||
}
|
||||
|
||||
// Return only notifs LOWER (ie., older) than maxID.
|
||||
q = q.Where("? < ?", bun.Ident("notification.id"), maxID)
|
||||
|
||||
if sinceID != "" {
|
||||
// Return only notifs HIGHER (ie., newer) than sinceID.
|
||||
q = q.Where("? > ?", bun.Ident("notification.id"), sinceID)
|
||||
if maxID != "" {
|
||||
// Return only notifs LOWER (ie., older) than maxID.
|
||||
q = q.Where("? < ?", bun.Ident("notification.id"), maxID)
|
||||
}
|
||||
|
||||
if minID != "" {
|
||||
// Return only notifs HIGHER (ie., newer) than minID.
|
||||
q = q.Where("? > ?", bun.Ident("notification.id"), minID)
|
||||
|
||||
frontToBack = false // page up
|
||||
}
|
||||
|
||||
if len(types) > 0 {
|
||||
|
|
@ -251,12 +239,12 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if frontToBack {
|
||||
// Page down.
|
||||
q = q.Order("notification.id DESC")
|
||||
} else {
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.Order("notification.id ASC")
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.Order("notification.id DESC")
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, ¬ifIDs); err != nil {
|
||||
|
|
@ -269,11 +257,8 @@ func (n *notificationDB) GetAccountNotifications(
|
|||
|
||||
// If we're paging up, we still want notifications
|
||||
// to be sorted by ID desc, so reverse ids slice.
|
||||
// https://zchee.github.io/golang-wiki/SliceTricks/#reversing
|
||||
if !frontToBack {
|
||||
for l, r := 0, len(notifIDs)-1; l < r; l, r = l+1, r-1 {
|
||||
notifIDs[l], notifIDs[r] = notifIDs[r], notifIDs[l]
|
||||
}
|
||||
if order == paging.OrderAscending {
|
||||
slices.Reverse(notifIDs)
|
||||
}
|
||||
|
||||
// Fetch notification models by their IDs.
|
||||
|
|
@ -303,7 +288,7 @@ func (n *notificationDB) DeleteNotificationByID(ctx context.Context, id string)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationDB) DeleteNotifications(ctx context.Context, types []string, targetAccountID string, originAccountID string) error {
|
||||
func (n *notificationDB) DeleteNotifications(ctx context.Context, types []gtsmodel.NotificationType, targetAccountID string, originAccountID string) error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return gtserror.New("one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue