mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-07 16:48:07 -06:00
Merge branch 'main' into 2fa
This commit is contained in:
commit
c6c212fb81
271 changed files with 7226 additions and 11019 deletions
|
|
@ -376,10 +376,9 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
|
|||
suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
|
||||
suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
|
||||
suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note)
|
||||
suite.EqualValues(requestingAccount.Memorial, dbUpdatedAccount.Memorial)
|
||||
suite.EqualValues(requestingAccount.MemorializedAt, dbUpdatedAccount.MemorializedAt)
|
||||
suite.EqualValues(requestingAccount.AlsoKnownAsURIs, dbUpdatedAccount.AlsoKnownAsURIs)
|
||||
suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI)
|
||||
suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot)
|
||||
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
|
||||
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable)
|
||||
suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
|
|||
suite.Equal(testAccount.Username, apimodelAccount.Acct)
|
||||
suite.Equal(testAccount.DisplayName, apimodelAccount.DisplayName)
|
||||
suite.Equal(*testAccount.Locked, apimodelAccount.Locked)
|
||||
suite.Equal(*testAccount.Bot, apimodelAccount.Bot)
|
||||
suite.False(apimodelAccount.Bot)
|
||||
suite.WithinDuration(testAccount.CreatedAt, createdAt, 30*time.Second) // we lose a bit of accuracy serializing so fuzz this a bit
|
||||
suite.Equal(testAccount.URL, apimodelAccount.URL)
|
||||
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", apimodelAccount.Avatar)
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"bot": true,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@localhost:8080",
|
||||
|
|
|
|||
|
|
@ -102,12 +102,14 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
|||
attachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler)
|
||||
attachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler)
|
||||
attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
|
||||
attachHandler(http.MethodPut, DomainBlocksPathWithID, m.DomainBlockUpdatePUTHandler)
|
||||
attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)
|
||||
|
||||
// domain allow stuff
|
||||
attachHandler(http.MethodPost, DomainAllowsPath, m.DomainAllowsPOSTHandler)
|
||||
attachHandler(http.MethodGet, DomainAllowsPath, m.DomainAllowsGETHandler)
|
||||
attachHandler(http.MethodGet, DomainAllowsPathWithID, m.DomainAllowGETHandler)
|
||||
attachHandler(http.MethodPut, DomainAllowsPathWithID, m.DomainAllowUpdatePUTHandler)
|
||||
attachHandler(http.MethodDelete, DomainAllowsPathWithID, m.DomainAllowDELETEHandler)
|
||||
|
||||
// domain permission draft stuff
|
||||
|
|
|
|||
91
internal/api/client/admin/domainallowupdate.go
Normal file
91
internal/api/client/admin/domainallowupdate.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// 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 (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// DomainAllowUpdatePUTHandler swagger:operation PUT /api/v1/admin/domain_allows/{id} domainAllowUpdate
|
||||
//
|
||||
// Update a single domain allow.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the domain allow.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// 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 allow.
|
||||
// This will be displayed alongside the domain allow if you choose to share allows.
|
||||
// type: string
|
||||
// -
|
||||
// name: private_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Private comment about this domain allow. Will only be shown to other admins, so this
|
||||
// is a useful way of internally keeping track of why a certain domain ended up allowed.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin:write:domain_allows
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The updated domain allow.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '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) DomainAllowUpdatePUTHandler(c *gin.Context) {
|
||||
m.updateDomainPermission(c, gtsmodel.DomainPermissionAllow)
|
||||
}
|
||||
91
internal/api/client/admin/domainblockupdate.go
Normal file
91
internal/api/client/admin/domainblockupdate.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// 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 (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// DomainBlockUpdatePUTHandler swagger:operation PUT /api/v1/admin/domain_blocks/{id} domainBlockUpdate
|
||||
//
|
||||
// Update a single domain block.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the domain block.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// 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 block.
|
||||
// This will be displayed alongside the domain block if you choose to share blocks.
|
||||
// type: string
|
||||
// -
|
||||
// name: private_comment
|
||||
// in: formData
|
||||
// description: >-
|
||||
// Private comment about this domain block. Will only be shown to other admins, so this
|
||||
// is a useful way of internally keeping track of why a certain domain ended up blocked.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin:write:domain_blocks
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The updated domain block.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainPermission"
|
||||
// '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) DomainBlockUpdatePUTHandler(c *gin.Context) {
|
||||
m.updateDomainPermission(c, gtsmodel.DomainPermissionBlock)
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import (
|
|||
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/util"
|
||||
)
|
||||
|
||||
type singleDomainPermCreate func(
|
||||
|
|
@ -112,7 +113,7 @@ func (m *Module) createDomainPermissions(
|
|||
if importing && form.Domains.Size == 0 {
|
||||
err = errors.New("import was specified but list of domains is empty")
|
||||
} else if !importing && form.Domain == "" {
|
||||
err = errors.New("empty domain provided")
|
||||
err = errors.New("no domain provided")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -122,14 +123,14 @@ func (m *Module) createDomainPermissions(
|
|||
|
||||
if !importing {
|
||||
// Single domain permission creation.
|
||||
domainBlock, _, errWithCode := single(
|
||||
perm, _, errWithCode := single(
|
||||
c.Request.Context(),
|
||||
permType,
|
||||
authed.Account,
|
||||
form.Domain,
|
||||
form.Obfuscate,
|
||||
form.PublicComment,
|
||||
form.PrivateComment,
|
||||
util.PtrOrZero(form.Obfuscate),
|
||||
util.PtrOrZero(form.PublicComment),
|
||||
util.PtrOrZero(form.PrivateComment),
|
||||
"", // No sub ID for single perm creation.
|
||||
)
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ func (m *Module) createDomainPermissions(
|
|||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, domainBlock)
|
||||
apiutil.JSON(c, http.StatusOK, perm)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -177,6 +178,82 @@ func (m *Module) createDomainPermissions(
|
|||
apiutil.JSON(c, http.StatusOK, domainPerms)
|
||||
}
|
||||
|
||||
func (m *Module) updateDomainPermission(
|
||||
c *gin.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
) {
|
||||
// Scope differs based on permType.
|
||||
var requireScope apiutil.Scope
|
||||
if permType == gtsmodel.DomainPermissionBlock {
|
||||
requireScope = apiutil.ScopeAdminWriteDomainBlocks
|
||||
} else {
|
||||
requireScope = apiutil.ScopeAdminWriteDomainAllows
|
||||
}
|
||||
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
true, true, true, true,
|
||||
requireScope,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, 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
|
||||
}
|
||||
|
||||
permID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, 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.Obfuscate == nil &&
|
||||
form.PrivateComment == nil &&
|
||||
form.PublicComment == nil {
|
||||
const errText = "empty form submitted"
|
||||
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
perm, errWithCode := m.processor.Admin().DomainPermissionUpdate(
|
||||
c.Request.Context(),
|
||||
permType,
|
||||
permID,
|
||||
form.Obfuscate,
|
||||
form.PublicComment,
|
||||
form.PrivateComment,
|
||||
nil, // Can't update perm sub ID this way yet.
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, perm)
|
||||
}
|
||||
|
||||
// deleteDomainPermission deletes a single domain permission (block or allow).
|
||||
func (m *Module) deleteDomainPermission(
|
||||
c *gin.Context,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
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/util"
|
||||
)
|
||||
|
||||
// DomainPermissionDraftsPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts domainPermissionDraftCreate
|
||||
|
|
@ -148,9 +149,9 @@ func (m *Module) DomainPermissionDraftsPOSTHandler(c *gin.Context) {
|
|||
authed.Account,
|
||||
form.Domain,
|
||||
permType,
|
||||
form.Obfuscate,
|
||||
form.PublicComment,
|
||||
form.PrivateComment,
|
||||
util.PtrOrZero(form.Obfuscate),
|
||||
util.PtrOrZero(form.PublicComment),
|
||||
util.PtrOrZero(form.PrivateComment),
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
|
|
|
|||
|
|
@ -97,14 +97,21 @@ func (suite *DomainPermissionSubscriptionTestTestSuite) TestDomainPermissionSubs
|
|||
suite.Equal(`[
|
||||
{
|
||||
"domain": "bumfaces.net",
|
||||
"public_comment": "big jerks"
|
||||
"public_comment": "big jerks",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
},
|
||||
{
|
||||
"domain": "peepee.poopoo",
|
||||
"public_comment": "harassment"
|
||||
"public_comment": "harassment",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
},
|
||||
{
|
||||
"domain": "nothanks.com"
|
||||
"domain": "nothanks.com",
|
||||
"public_comment": "",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
}
|
||||
]`, dst.String())
|
||||
|
||||
|
|
@ -177,13 +184,22 @@ func (suite *DomainPermissionSubscriptionTestTestSuite) TestDomainPermissionSubs
|
|||
// Ensure expected.
|
||||
suite.Equal(`[
|
||||
{
|
||||
"domain": "bumfaces.net"
|
||||
"domain": "bumfaces.net",
|
||||
"public_comment": "",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
},
|
||||
{
|
||||
"domain": "peepee.poopoo"
|
||||
"domain": "peepee.poopoo",
|
||||
"public_comment": "",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
},
|
||||
{
|
||||
"domain": "nothanks.com"
|
||||
"domain": "nothanks.com",
|
||||
"public_comment": "",
|
||||
"obfuscate": false,
|
||||
"private_comment": ""
|
||||
}
|
||||
]`, dst.String())
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspended() {
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"public_comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthori
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"public_comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAll() {
|
|||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"public_comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
@ -263,12 +263,12 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated()
|
|||
{
|
||||
"domain": "o*g.*u**.t**.*or*t.*r**ev**",
|
||||
"suspended_at": "2021-06-09T10:34:55.000Z",
|
||||
"public_comment": "just absolutely the worst, wowza"
|
||||
"comment": "just absolutely the worst, wowza"
|
||||
},
|
||||
{
|
||||
"domain": "replyguys.com",
|
||||
"suspended_at": "2020-05-13T13:29:12.000Z",
|
||||
"public_comment": "reply-guying to tech posts"
|
||||
"comment": "reply-guying to tech posts"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/search"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
|
|
@ -1402,7 +1401,7 @@ func (suite *SearchGetTestSuite) TestSearchRemoteInstanceAccountPartial() {
|
|||
FollowersURI: "http://" + theirDomain + "/users/" + theirDomain + "/followers",
|
||||
FollowingURI: "http://" + theirDomain + "/users/" + theirDomain + "/following",
|
||||
FeaturedCollectionURI: "http://" + theirDomain + "/users/" + theirDomain + "/collections/featured",
|
||||
ActorType: ap.ActorPerson,
|
||||
ActorType: gtsmodel.AccountActorTypePerson,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
}); err != nil {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,13 @@ type Domain struct {
|
|||
// example: 2021-07-30T09:20:25+00:00
|
||||
SilencedAt string `json:"silenced_at,omitempty"`
|
||||
// If the domain is blocked, what's the publicly-stated reason for the block.
|
||||
// Alternative to `public_comment` to be used when serializing/deserializing via /api/v1/instance.
|
||||
// example: they smell
|
||||
PublicComment string `form:"public_comment" json:"public_comment,omitempty"`
|
||||
Comment *string `form:"comment" json:"comment,omitempty"`
|
||||
// If the domain is blocked, what's the publicly-stated reason for the block.
|
||||
// Alternative to `comment` to be used when serializing/deserializing NOT via /api/v1/instance.
|
||||
// example: they smell
|
||||
PublicComment *string `form:"public_comment" json:"public_comment,omitempty"`
|
||||
}
|
||||
|
||||
// DomainPermission represents a permission applied to one domain (explicit block/allow).
|
||||
|
|
@ -48,10 +53,10 @@ type DomainPermission struct {
|
|||
ID string `json:"id,omitempty"`
|
||||
// Obfuscate the domain name when serving this domain permission entry publicly.
|
||||
// example: false
|
||||
Obfuscate bool `json:"obfuscate,omitempty"`
|
||||
Obfuscate *bool `json:"obfuscate,omitempty"`
|
||||
// Private comment for this permission entry, visible to this instance's admins only.
|
||||
// example: they are poopoo
|
||||
PrivateComment string `json:"private_comment,omitempty"`
|
||||
PrivateComment *string `json:"private_comment,omitempty"`
|
||||
// If applicable, the ID of the subscription that caused this domain permission entry to be created.
|
||||
// example: 01FBW25TF5J67JW3HFHZCSD23K
|
||||
SubscriptionID string `json:"subscription_id,omitempty"`
|
||||
|
|
@ -80,14 +85,14 @@ type DomainPermissionRequest struct {
|
|||
// 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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||
|
|
@ -124,7 +123,7 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
|
|||
FollowingURI: "http://" + host + "/users/new_account_domain_user/following",
|
||||
FollowersURI: "http://" + host + "/users/new_account_domain_user/followers",
|
||||
FeaturedCollectionURI: "http://" + host + "/users/new_account_domain_user/collections/featured",
|
||||
ActorType: ap.ActorPerson,
|
||||
ActorType: gtsmodel.AccountActorTypePerson,
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
PublicKeyURI: "http://" + host + "/users/new_account_domain_user/main-key",
|
||||
|
|
|
|||
7
internal/cache/db.go
vendored
7
internal/cache/db.go
vendored
|
|
@ -306,13 +306,8 @@ func (c *Caches) initAccount() {
|
|||
Indices: []structr.IndexConfig{
|
||||
{Fields: "ID"},
|
||||
{Fields: "URI"},
|
||||
{Fields: "URL"},
|
||||
{Fields: "Username,Domain", AllowZero: true},
|
||||
{Fields: "PublicKeyURI"},
|
||||
{Fields: "InboxURI"},
|
||||
{Fields: "OutboxURI"},
|
||||
{Fields: "FollowersURI"},
|
||||
{Fields: "FollowingURI"},
|
||||
{Fields: "Username,Domain", AllowZero: true},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
|
|
|
|||
9
internal/cache/size.go
vendored
9
internal/cache/size.go
vendored
|
|
@ -240,13 +240,12 @@ func sizeofAccount() uintptr {
|
|||
DisplayName: exampleUsername,
|
||||
Note: exampleText,
|
||||
NoteRaw: exampleText,
|
||||
Memorial: func() *bool { ok := false; return &ok }(),
|
||||
MemorializedAt: exampleTime,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
FetchedAt: exampleTime,
|
||||
Bot: func() *bool { ok := true; return &ok }(),
|
||||
Locked: func() *bool { ok := true; return &ok }(),
|
||||
Discoverable: func() *bool { ok := false; return &ok }(),
|
||||
Locked: util.Ptr(true),
|
||||
Discoverable: util.Ptr(false),
|
||||
URI: exampleURI,
|
||||
URL: exampleURI,
|
||||
InboxURI: exampleURI,
|
||||
|
|
@ -254,7 +253,7 @@ func sizeofAccount() uintptr {
|
|||
FollowersURI: exampleURI,
|
||||
FollowingURI: exampleURI,
|
||||
FeaturedCollectionURI: exampleURI,
|
||||
ActorType: ap.ActorPerson,
|
||||
ActorType: gtsmodel.AccountActorTypePerson,
|
||||
PrivateKey: &rsa.PrivateKey{},
|
||||
PublicKey: &rsa.PublicKey{},
|
||||
PublicKeyURI: exampleURI,
|
||||
|
|
|
|||
|
|
@ -27,37 +27,37 @@ import (
|
|||
|
||||
// Account contains functions related to account getting/setting/creation.
|
||||
type Account interface {
|
||||
// GetAccountByID returns one account with the given ID, or an error if something goes wrong.
|
||||
// GetAccountByID returns one account with the given ID.
|
||||
GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountsByIDs returns accounts corresponding to given IDs.
|
||||
GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByURI returns one account with the given URI, or an error if something goes wrong.
|
||||
// GetAccountByURI returns one account with the given ActivityStreams URI.
|
||||
GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByURL returns one account with the given URL, or an error if something goes wrong.
|
||||
GetAccountByURL(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
// GetOneAccountByURL returns *one* account with the given ActivityStreams URL.
|
||||
// If more than one account has the given url, ErrMultipleEntries will be returned.
|
||||
GetOneAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByUsernameDomain returns one account with the given username and domain, or an error if something goes wrong.
|
||||
// GetAccountsByURL returns accounts with the given ActivityStreams URL.
|
||||
GetAccountsByURL(ctx context.Context, url string) ([]*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByUsernameDomain returns one account with the given username and domain.
|
||||
GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByPubkeyID returns one account with the given public key URI (ID), or an error if something goes wrong.
|
||||
// GetAccountByPubkeyID returns one account with the given public key URI (ID).
|
||||
GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByInboxURI returns one account with the given inbox_uri, or an error if something goes wrong.
|
||||
GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
// GetOneAccountByInboxURI returns one account with the given inbox_uri.
|
||||
// If more than one account has the given URL, ErrMultipleEntries will be returned.
|
||||
GetOneAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByOutboxURI returns one account with the given outbox_uri, or an error if something goes wrong.
|
||||
GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
// GetOneAccountByOutboxURI returns one account with the given outbox_uri.
|
||||
// If more than one account has the given uri, ErrMultipleEntries will be returned.
|
||||
GetOneAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByFollowingURI returns one account with the given following_uri, or an error if something goes wrong.
|
||||
GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByFollowersURI returns one account with the given followers_uri, or an error if something goes wrong.
|
||||
GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByMovedToURI returns any accounts with given moved_to_uri set.
|
||||
// GetAccountsByMovedToURI returns any accounts with given moved_to_uri set.
|
||||
GetAccountsByMovedToURI(ctx context.Context, uri string) ([]*gtsmodel.Account, error)
|
||||
|
||||
// GetAccounts returns accounts
|
||||
|
|
|
|||
|
|
@ -121,18 +121,46 @@ func (a *accountDB) GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.
|
|||
)
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error) {
|
||||
return a.getAccount(
|
||||
ctx,
|
||||
"URL",
|
||||
func(account *gtsmodel.Account) error {
|
||||
return a.db.NewSelect().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.url"), url).
|
||||
Scan(ctx)
|
||||
},
|
||||
url,
|
||||
)
|
||||
func (a *accountDB) GetOneAccountByURL(ctx context.Context, url string) (*gtsmodel.Account, error) {
|
||||
// Select IDs of all
|
||||
// accounts with this url.
|
||||
var ids []string
|
||||
if err := a.db.NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
||||
Column("account.id").
|
||||
Where("? = ?", bun.Ident("account.url"), url).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure exactly one account.
|
||||
if len(ids) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
if len(ids) > 1 {
|
||||
return nil, db.ErrMultipleEntries
|
||||
}
|
||||
|
||||
return a.GetAccountByID(ctx, ids[0])
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountsByURL(ctx context.Context, url string) ([]*gtsmodel.Account, error) {
|
||||
// Select IDs of all
|
||||
// accounts with this url.
|
||||
var ids []string
|
||||
if err := a.db.NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
||||
Column("account.id").
|
||||
Where("? = ?", bun.Ident("account.url"), url).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
return a.GetAccountsByIDs(ctx, ids)
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByUsernameDomain(ctx context.Context, username string, domain string) (*gtsmodel.Account, error) {
|
||||
|
|
@ -184,60 +212,50 @@ func (a *accountDB) GetAccountByPubkeyID(ctx context.Context, id string) (*gtsmo
|
|||
)
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByInboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
|
||||
return a.getAccount(
|
||||
ctx,
|
||||
"InboxURI",
|
||||
func(account *gtsmodel.Account) error {
|
||||
return a.db.NewSelect().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.inbox_uri"), uri).
|
||||
Scan(ctx)
|
||||
},
|
||||
uri,
|
||||
)
|
||||
func (a *accountDB) GetOneAccountByInboxURI(ctx context.Context, inboxURI string) (*gtsmodel.Account, error) {
|
||||
// Select IDs of all accounts
|
||||
// with this inbox_uri.
|
||||
var ids []string
|
||||
if err := a.db.NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
||||
Column("account.id").
|
||||
Where("? = ?", bun.Ident("account.inbox_uri"), inboxURI).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure exactly one account.
|
||||
if len(ids) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
if len(ids) > 1 {
|
||||
return nil, db.ErrMultipleEntries
|
||||
}
|
||||
|
||||
return a.GetAccountByID(ctx, ids[0])
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByOutboxURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
|
||||
return a.getAccount(
|
||||
ctx,
|
||||
"OutboxURI",
|
||||
func(account *gtsmodel.Account) error {
|
||||
return a.db.NewSelect().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.outbox_uri"), uri).
|
||||
Scan(ctx)
|
||||
},
|
||||
uri,
|
||||
)
|
||||
}
|
||||
func (a *accountDB) GetOneAccountByOutboxURI(ctx context.Context, outboxURI string) (*gtsmodel.Account, error) {
|
||||
// Select IDs of all accounts
|
||||
// with this outbox_uri.
|
||||
var ids []string
|
||||
if err := a.db.NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
||||
Column("account.id").
|
||||
Where("? = ?", bun.Ident("account.outbox_uri"), outboxURI).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
|
||||
return a.getAccount(
|
||||
ctx,
|
||||
"FollowersURI",
|
||||
func(account *gtsmodel.Account) error {
|
||||
return a.db.NewSelect().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.followers_uri"), uri).
|
||||
Scan(ctx)
|
||||
},
|
||||
uri,
|
||||
)
|
||||
}
|
||||
// Ensure exactly one account.
|
||||
if len(ids) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
if len(ids) > 1 {
|
||||
return nil, db.ErrMultipleEntries
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountByFollowingURI(ctx context.Context, uri string) (*gtsmodel.Account, error) {
|
||||
return a.getAccount(
|
||||
ctx,
|
||||
"FollowingURI",
|
||||
func(account *gtsmodel.Account) error {
|
||||
return a.db.NewSelect().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.following_uri"), uri).
|
||||
Scan(ctx)
|
||||
},
|
||||
uri,
|
||||
)
|
||||
return a.GetAccountByID(ctx, ids[0])
|
||||
}
|
||||
|
||||
func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, error) {
|
||||
|
|
@ -587,7 +605,11 @@ func (a *accountDB) GetAccounts(
|
|||
return a.state.DB.GetAccountsByIDs(ctx, accountIDs)
|
||||
}
|
||||
|
||||
func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Account) error, keyParts ...any) (*gtsmodel.Account, error) {
|
||||
func (a *accountDB) getAccount(
|
||||
ctx context.Context,
|
||||
lookup string,
|
||||
dbQuery func(*gtsmodel.Account) error, keyParts ...any,
|
||||
) (*gtsmodel.Account, error) {
|
||||
// Fetch account from database cache with loader callback
|
||||
account, err := a.state.Caches.DB.Account.LoadOne(lookup, func() (*gtsmodel.Account, error) {
|
||||
var account gtsmodel.Account
|
||||
|
|
|
|||
|
|
@ -32,11 +32,10 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type AccountTestSuite struct {
|
||||
|
|
@ -255,7 +254,20 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
|
|||
if account.URL == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
return suite.db.GetAccountByURL(ctx, account.URL)
|
||||
return suite.db.GetOneAccountByURL(ctx, account.URL)
|
||||
},
|
||||
|
||||
"url_multi": func() (*gtsmodel.Account, error) {
|
||||
if account.URL == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
|
||||
accounts, err := suite.db.GetAccountsByURL(ctx, account.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accounts[0], nil
|
||||
},
|
||||
|
||||
"username@domain": func() (*gtsmodel.Account, error) {
|
||||
|
|
@ -281,28 +293,14 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
|
|||
if account.InboxURI == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
return suite.db.GetAccountByInboxURI(ctx, account.InboxURI)
|
||||
return suite.db.GetOneAccountByInboxURI(ctx, account.InboxURI)
|
||||
},
|
||||
|
||||
"outbox_uri": func() (*gtsmodel.Account, error) {
|
||||
if account.OutboxURI == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
return suite.db.GetAccountByOutboxURI(ctx, account.OutboxURI)
|
||||
},
|
||||
|
||||
"following_uri": func() (*gtsmodel.Account, error) {
|
||||
if account.FollowingURI == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
return suite.db.GetAccountByFollowingURI(ctx, account.FollowingURI)
|
||||
},
|
||||
|
||||
"followers_uri": func() (*gtsmodel.Account, error) {
|
||||
if account.FollowersURI == "" {
|
||||
return nil, sentinelErr
|
||||
}
|
||||
return suite.db.GetAccountByFollowersURI(ctx, account.FollowersURI)
|
||||
return suite.db.GetOneAccountByOutboxURI(ctx, account.OutboxURI)
|
||||
},
|
||||
} {
|
||||
|
||||
|
|
@ -345,71 +343,37 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
|
|||
}
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestUpdateAccount() {
|
||||
func (suite *AccountTestSuite) TestGetAccountsByURLMulti() {
|
||||
ctx := context.Background()
|
||||
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
testAccount.DisplayName = "new display name!"
|
||||
testAccount.EmojiIDs = []string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}
|
||||
|
||||
err := suite.db.UpdateAccount(ctx, testAccount)
|
||||
suite.NoError(err)
|
||||
|
||||
updated, err := suite.db.GetAccountByID(ctx, testAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.Equal("new display name!", updated.DisplayName)
|
||||
suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, updated.EmojiIDs)
|
||||
suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second)
|
||||
|
||||
// get account without cache + make sure it's really in the db as desired
|
||||
dbService, ok := suite.db.(*bundb.DBService)
|
||||
if !ok {
|
||||
panic("db was not *bundb.DBService")
|
||||
// Update admin account to have the same url as zork.
|
||||
testAccount1 := suite.testAccounts["local_account_1"]
|
||||
testAccount2 := new(gtsmodel.Account)
|
||||
*testAccount2 = *suite.testAccounts["admin_account"]
|
||||
testAccount2.URL = testAccount1.URL
|
||||
if err := suite.state.DB.UpdateAccount(ctx, testAccount2, "url"); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
noCache := >smodel.Account{}
|
||||
err = dbService.DB().
|
||||
NewSelect().
|
||||
Model(noCache).
|
||||
Where("? = ?", bun.Ident("account.id"), testAccount.ID).
|
||||
Relation("AvatarMediaAttachment").
|
||||
Relation("HeaderMediaAttachment").
|
||||
Relation("Emojis").
|
||||
Scan(ctx)
|
||||
// Select all accounts with that URL.
|
||||
// Should return 2.
|
||||
accounts, err := suite.state.DB.GetAccountsByURL(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
testAccount1.URL,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.Len(accounts, 2)
|
||||
|
||||
suite.NoError(err)
|
||||
suite.Equal("new display name!", noCache.DisplayName)
|
||||
suite.Equal([]string{"01GD36ZKWTKY3T1JJ24JR7KY1Q", "01GD36ZV904SHBHNAYV6DX5QEF"}, noCache.EmojiIDs)
|
||||
suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second)
|
||||
suite.NotNil(noCache.AvatarMediaAttachment)
|
||||
suite.NotNil(noCache.HeaderMediaAttachment)
|
||||
|
||||
// update again to remove emoji associations
|
||||
testAccount.EmojiIDs = []string{}
|
||||
|
||||
err = suite.db.UpdateAccount(ctx, testAccount)
|
||||
suite.NoError(err)
|
||||
|
||||
updated, err = suite.db.GetAccountByID(ctx, testAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.Equal("new display name!", updated.DisplayName)
|
||||
suite.Empty(updated.EmojiIDs)
|
||||
suite.WithinDuration(time.Now(), updated.UpdatedAt, 5*time.Second)
|
||||
|
||||
err = dbService.DB().
|
||||
NewSelect().
|
||||
Model(noCache).
|
||||
Where("? = ?", bun.Ident("account.id"), testAccount.ID).
|
||||
Relation("AvatarMediaAttachment").
|
||||
Relation("HeaderMediaAttachment").
|
||||
Relation("Emojis").
|
||||
Scan(ctx)
|
||||
|
||||
suite.NoError(err)
|
||||
suite.Equal("new display name!", noCache.DisplayName)
|
||||
suite.Empty(noCache.EmojiIDs)
|
||||
suite.WithinDuration(time.Now(), noCache.UpdatedAt, 5*time.Second)
|
||||
// Try to select one account with that URL.
|
||||
// Should error.
|
||||
account, err := suite.state.DB.GetOneAccountByURL(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
testAccount1.URL,
|
||||
)
|
||||
suite.Nil(account)
|
||||
suite.ErrorIs(err, db.ErrMultipleEntries)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
||||
|
|
@ -422,7 +386,7 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
|||
Domain: "example.org",
|
||||
URI: "https://example.org/users/test_service",
|
||||
URL: "https://example.org/@test_service",
|
||||
ActorType: ap.ActorService,
|
||||
ActorType: gtsmodel.AccountActorTypeService,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: "https://example.org/users/test_service#main-key",
|
||||
}
|
||||
|
|
@ -433,7 +397,6 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
|||
suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second)
|
||||
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
|
||||
suite.True(*newAccount.Locked)
|
||||
suite.False(*newAccount.Bot)
|
||||
suite.False(*newAccount.Discoverable)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
|
|
@ -131,7 +130,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
|
|||
FollowingURI: uris.FollowingURI,
|
||||
FollowersURI: uris.FollowersURI,
|
||||
FeaturedCollectionURI: uris.FeaturedCollectionURI,
|
||||
ActorType: ap.ActorPerson,
|
||||
ActorType: gtsmodel.AccountActorTypePerson,
|
||||
PrivateKey: privKey,
|
||||
PublicKey: &privKey.PublicKey,
|
||||
PublicKeyURI: uris.PublicKeyURI,
|
||||
|
|
@ -283,7 +282,7 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) error {
|
|||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: newAccountURIs.PublicKeyURI,
|
||||
ActorType: ap.ActorPerson,
|
||||
ActorType: gtsmodel.AccountActorTypeService,
|
||||
URI: newAccountURIs.UserURI,
|
||||
InboxURI: newAccountURIs.InboxURI,
|
||||
OutboxURI: newAccountURIs.OutboxURI,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
|
|||
URL: "https://example.org/@test",
|
||||
InboxURI: "https://example.org/users/test/inbox",
|
||||
OutboxURI: "https://example.org/users/test/outbox",
|
||||
ActorType: "Person",
|
||||
ActorType: gtsmodel.AccountActorTypePerson,
|
||||
PublicKeyURI: "https://example.org/test#main-key",
|
||||
PublicKey: &key.PublicKey,
|
||||
}
|
||||
|
|
@ -87,7 +87,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
|
|||
suite.Empty(a.NoteRaw)
|
||||
suite.Empty(a.AlsoKnownAsURIs)
|
||||
suite.Empty(a.MovedToURI)
|
||||
suite.False(*a.Bot)
|
||||
// Locked is especially important, since it's a bool that defaults
|
||||
// to true, which is why we use pointers for bools in the first place
|
||||
suite.True(*a.Locked)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type domainDB struct {
|
|||
state *state.State
|
||||
}
|
||||
|
||||
func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) (err error) {
|
||||
func (d *domainDB) PutDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) (err error) {
|
||||
// Normalize the domain as punycode, note the extra
|
||||
// validation step for domain name write operations.
|
||||
allow.Domain, err = util.PunifySafely(allow.Domain)
|
||||
|
|
@ -162,7 +162,7 @@ func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error {
|
||||
func (d *domainDB) PutDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error {
|
||||
var err error
|
||||
|
||||
// Normalize the domain as punycode, note the extra
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func (suite *DomainTestSuite) TestIsDomainBlocked() {
|
|||
suite.NoError(err)
|
||||
suite.False(blocked)
|
||||
|
||||
err = suite.db.CreateDomainBlock(ctx, domainBlock)
|
||||
err = suite.db.PutDomainBlock(ctx, domainBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
// domain block now exists
|
||||
|
|
@ -75,7 +75,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWithAllow() {
|
|||
suite.False(blocked)
|
||||
|
||||
// Block this domain.
|
||||
if err := suite.db.CreateDomainBlock(ctx, domainBlock); err != nil {
|
||||
if err := suite.db.PutDomainBlock(ctx, domainBlock); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWithAllow() {
|
|||
CreatedByAccount: suite.testAccounts["admin_account"],
|
||||
}
|
||||
|
||||
if err := suite.db.CreateDomainAllow(ctx, domainAllow); err != nil {
|
||||
if err := suite.db.PutDomainAllow(ctx, domainAllow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedWildcard() {
|
|||
suite.NoError(err)
|
||||
suite.False(blocked)
|
||||
|
||||
err = suite.db.CreateDomainBlock(ctx, domainBlock)
|
||||
err = suite.db.PutDomainBlock(ctx, domainBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
// Start with the base block domain
|
||||
|
|
@ -164,7 +164,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedNonASCII() {
|
|||
suite.NoError(err)
|
||||
suite.False(blocked)
|
||||
|
||||
err = suite.db.CreateDomainBlock(ctx, domainBlock)
|
||||
err = suite.db.PutDomainBlock(ctx, domainBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
// domain block now exists
|
||||
|
|
@ -200,7 +200,7 @@ func (suite *DomainTestSuite) TestIsDomainBlockedNonASCII2() {
|
|||
suite.NoError(err)
|
||||
suite.False(blocked)
|
||||
|
||||
err = suite.db.CreateDomainBlock(ctx, domainBlock)
|
||||
err = suite.db.PutDomainBlock(ctx, domainBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
// domain block now exists
|
||||
|
|
@ -232,7 +232,7 @@ func (suite *DomainTestSuite) TestIsOtherDomainBlockedWildcardAndExplicit() {
|
|||
}
|
||||
|
||||
for _, block := range blocks {
|
||||
if err := suite.db.CreateDomainBlock(ctx, block); err != nil {
|
||||
if err := suite.db.PutDomainBlock(ctx, block); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func (suite *DomainPermissionSubscriptionTestSuite) TestCount() {
|
|||
|
||||
// Whack the perms in the db.
|
||||
for _, perm := range perms {
|
||||
if err := suite.state.DB.CreateDomainBlock(ctx, perm); err != nil {
|
||||
if err := suite.state.DB.PutDomainBlock(ctx, perm); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,12 +81,13 @@ func init() {
|
|||
return err
|
||||
}
|
||||
|
||||
// Set instance app
|
||||
// ID on all users.
|
||||
// Set instance app ID on
|
||||
// users where it's null.
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Table("users").
|
||||
Set("? = ?", bun.Ident("created_by_application_id"), instanceAppID).
|
||||
Where("? IS NULL", bun.Ident("created_by_application_id")).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,398 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/new"
|
||||
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/old"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, bdb *bun.DB) error {
|
||||
log.Info(ctx, "converting accounts to new model; this may take a while, please don't interrupt!")
|
||||
|
||||
return bdb.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
var (
|
||||
// We have to use different
|
||||
// syntax for this query
|
||||
// depending on dialect.
|
||||
dbDialect = tx.Dialect().Name()
|
||||
|
||||
// ID for paging.
|
||||
maxID string
|
||||
|
||||
// Batch size for
|
||||
// selecting + updating.
|
||||
batchsz = 100
|
||||
|
||||
// Number of accounts
|
||||
// updated so far.
|
||||
updated int
|
||||
|
||||
// We need to know our own host
|
||||
// for updating instance account.
|
||||
host = config.GetHost()
|
||||
)
|
||||
|
||||
// Create the new accounts table.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
ModelTableExpr("new_accounts").
|
||||
Model(&new_gtsmodel.Account{}).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Count number of accounts
|
||||
// we need to update.
|
||||
total, err := tx.
|
||||
NewSelect().
|
||||
Table("accounts").
|
||||
Count(ctx)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a subquery for
|
||||
// Postgres to reuse.
|
||||
var orderQPG *bun.RawQuery
|
||||
if dbDialect == dialect.PG {
|
||||
orderQPG = tx.NewRaw(
|
||||
"(COALESCE(?, ?) || ? || ?) COLLATE ?",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
bun.Ident("C"),
|
||||
)
|
||||
}
|
||||
|
||||
var orderQSqlite *bun.RawQuery
|
||||
if dbDialect == dialect.SQLite {
|
||||
orderQSqlite = tx.NewRaw(
|
||||
"(COALESCE(?, ?) || ? || ?)",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
)
|
||||
}
|
||||
|
||||
for {
|
||||
// Batch of old model account IDs to select.
|
||||
oldAccountIDs := make([]string, 0, batchsz)
|
||||
|
||||
// Start building IDs query.
|
||||
idsQ := tx.
|
||||
NewSelect().
|
||||
Table("accounts").
|
||||
Column("id").
|
||||
Limit(batchsz)
|
||||
|
||||
if dbDialect == dialect.SQLite {
|
||||
// For SQLite we can just select
|
||||
// our indexed expression once
|
||||
// as a column alias.
|
||||
idsQ = idsQ.
|
||||
ColumnExpr(
|
||||
"(COALESCE(?, ?) || ? || ?) AS ?",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
bun.Ident("domain_username"),
|
||||
)
|
||||
}
|
||||
|
||||
// Return only accounts with `[domain]/@[username]`
|
||||
// later in the alphabet (a-z) than provided maxID.
|
||||
if maxID != "" {
|
||||
if dbDialect == dialect.SQLite {
|
||||
idsQ = idsQ.Where("? > ?", bun.Ident("domain_username"), maxID)
|
||||
} else {
|
||||
idsQ = idsQ.Where("? > ?", orderQPG, maxID)
|
||||
}
|
||||
}
|
||||
|
||||
// Page down.
|
||||
// It's counterintuitive because it
|
||||
// says ASC in the query, but we're
|
||||
// going forwards in the alphabet,
|
||||
// and z > a in a string comparison.
|
||||
if dbDialect == dialect.SQLite {
|
||||
idsQ = idsQ.OrderExpr("? ASC", bun.Ident("domain_username"))
|
||||
} else {
|
||||
idsQ = idsQ.OrderExpr("? ASC", orderQPG)
|
||||
}
|
||||
|
||||
// Select this batch, providing a
|
||||
// slice to throw away username_domain.
|
||||
err := idsQ.Scan(ctx, &oldAccountIDs, new([]string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := len(oldAccountIDs)
|
||||
if len(oldAccountIDs) == 0 {
|
||||
// Nothing left
|
||||
// to update.
|
||||
break
|
||||
}
|
||||
|
||||
// Get ready to select old accounts by their IDs.
|
||||
oldAccounts := make([]*old_gtsmodel.Account, 0, l)
|
||||
batchQ := tx.
|
||||
NewSelect().
|
||||
Model(&oldAccounts).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(oldAccountIDs))
|
||||
|
||||
// Order batch by usernameDomain
|
||||
// to ensure paging consistent.
|
||||
if dbDialect == dialect.SQLite {
|
||||
batchQ = batchQ.OrderExpr("? ASC", orderQSqlite)
|
||||
} else {
|
||||
batchQ = batchQ.OrderExpr("? ASC", orderQPG)
|
||||
}
|
||||
|
||||
// Select old accounts.
|
||||
if err := batchQ.Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert old accounts into new accounts.
|
||||
newAccounts := make([]*new_gtsmodel.Account, 0, l)
|
||||
for _, oldAccount := range oldAccounts {
|
||||
|
||||
var actorType new_gtsmodel.AccountActorType
|
||||
if oldAccount.Domain == "" && oldAccount.Username == host {
|
||||
// This is our instance account, override actor
|
||||
// type to Service, as previously it was just person.
|
||||
actorType = new_gtsmodel.AccountActorTypeService
|
||||
} else {
|
||||
// Not our instance account, just parse new actor type.
|
||||
actorType = new_gtsmodel.ParseAccountActorType(oldAccount.ActorType)
|
||||
}
|
||||
|
||||
if actorType == new_gtsmodel.AccountActorTypeUnknown {
|
||||
// This should not really happen, but it if does
|
||||
// just warn + set to person rather than failing.
|
||||
log.Warnf(ctx,
|
||||
"account %s actor type %s was not a recognized actor type, falling back to Person",
|
||||
oldAccount.ID, oldAccount.ActorType,
|
||||
)
|
||||
actorType = new_gtsmodel.AccountActorTypePerson
|
||||
}
|
||||
|
||||
newAccount := &new_gtsmodel.Account{
|
||||
ID: oldAccount.ID,
|
||||
CreatedAt: oldAccount.CreatedAt,
|
||||
UpdatedAt: oldAccount.UpdatedAt,
|
||||
FetchedAt: oldAccount.FetchedAt,
|
||||
Username: oldAccount.Username,
|
||||
Domain: oldAccount.Domain,
|
||||
AvatarMediaAttachmentID: oldAccount.AvatarMediaAttachmentID,
|
||||
AvatarRemoteURL: oldAccount.AvatarRemoteURL,
|
||||
HeaderMediaAttachmentID: oldAccount.HeaderMediaAttachmentID,
|
||||
HeaderRemoteURL: oldAccount.HeaderRemoteURL,
|
||||
DisplayName: oldAccount.DisplayName,
|
||||
EmojiIDs: oldAccount.EmojiIDs,
|
||||
Fields: oldAccount.Fields,
|
||||
FieldsRaw: oldAccount.FieldsRaw,
|
||||
Note: oldAccount.Note,
|
||||
NoteRaw: oldAccount.NoteRaw,
|
||||
AlsoKnownAsURIs: oldAccount.AlsoKnownAsURIs,
|
||||
MovedToURI: oldAccount.MovedToURI,
|
||||
MoveID: oldAccount.MoveID,
|
||||
Locked: oldAccount.Locked,
|
||||
Discoverable: oldAccount.Discoverable,
|
||||
URI: oldAccount.URI,
|
||||
URL: oldAccount.URL,
|
||||
InboxURI: oldAccount.InboxURI,
|
||||
SharedInboxURI: oldAccount.SharedInboxURI,
|
||||
OutboxURI: oldAccount.OutboxURI,
|
||||
FollowingURI: oldAccount.FollowingURI,
|
||||
FollowersURI: oldAccount.FollowersURI,
|
||||
FeaturedCollectionURI: oldAccount.FeaturedCollectionURI,
|
||||
ActorType: actorType,
|
||||
PrivateKey: oldAccount.PrivateKey,
|
||||
PublicKey: oldAccount.PublicKey,
|
||||
PublicKeyURI: oldAccount.PublicKeyURI,
|
||||
PublicKeyExpiresAt: oldAccount.PublicKeyExpiresAt,
|
||||
SensitizedAt: oldAccount.SensitizedAt,
|
||||
SilencedAt: oldAccount.SilencedAt,
|
||||
SuspendedAt: oldAccount.SuspendedAt,
|
||||
SuspensionOrigin: oldAccount.SuspensionOrigin,
|
||||
}
|
||||
|
||||
newAccounts = append(newAccounts, newAccount)
|
||||
}
|
||||
|
||||
// Insert this batch of accounts.
|
||||
res, err := tx.
|
||||
NewInsert().
|
||||
Model(&newAccounts).
|
||||
Returning("").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to updated count.
|
||||
updated += int(rowsAffected)
|
||||
if updated == total {
|
||||
// Done.
|
||||
break
|
||||
}
|
||||
|
||||
// Set next page.
|
||||
fromAcct := oldAccounts[l-1]
|
||||
maxID = fromAcct.Domain + "/@" + fromAcct.Username
|
||||
|
||||
// Log helpful message to admin.
|
||||
log.Infof(ctx,
|
||||
"migrated %d of %d accounts (next page will be from %s)",
|
||||
updated, total, maxID,
|
||||
)
|
||||
}
|
||||
|
||||
if total != int(updated) {
|
||||
// Return error here in order to rollback the whole transaction.
|
||||
return fmt.Errorf("total=%d does not match updated=%d", total, updated)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "finished migrating %d accounts", total)
|
||||
|
||||
// Drop the old table.
|
||||
log.Info(ctx, "dropping old accounts table")
|
||||
if _, err := tx.
|
||||
NewDropTable().
|
||||
Table("accounts").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename new table to old table.
|
||||
log.Info(ctx, "renaming new accounts table")
|
||||
if _, err := tx.
|
||||
ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? RENAME TO ?",
|
||||
bun.Ident("new_accounts"),
|
||||
bun.Ident("accounts"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add all account indexes to the new table.
|
||||
log.Info(ctx, "recreating indexes on new accounts table")
|
||||
for index, columns := range map[string][]string{
|
||||
"accounts_domain_idx": {"domain"},
|
||||
"accounts_uri_idx": {"uri"},
|
||||
"accounts_url_idx": {"url"},
|
||||
"accounts_inbox_uri_idx": {"inbox_uri"},
|
||||
"accounts_outbox_uri_idx": {"outbox_uri"},
|
||||
"accounts_followers_uri_idx": {"followers_uri"},
|
||||
"accounts_following_uri_idx": {"following_uri"},
|
||||
} {
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("accounts").
|
||||
Index(index).
|
||||
Column(columns...).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if dbDialect == dialect.PG {
|
||||
log.Info(ctx, "moving postgres constraints from old table to new table")
|
||||
|
||||
type spec struct {
|
||||
old string
|
||||
new string
|
||||
columns []string
|
||||
}
|
||||
|
||||
// Rename uniqueness constraints from
|
||||
// "new_accounts_*" to "accounts_*".
|
||||
for _, spec := range []spec{
|
||||
{
|
||||
old: "new_accounts_pkey",
|
||||
new: "accounts_pkey",
|
||||
columns: []string{"id"},
|
||||
},
|
||||
{
|
||||
old: "new_accounts_uri_key",
|
||||
new: "accounts_uri_key",
|
||||
columns: []string{"uri"},
|
||||
},
|
||||
{
|
||||
old: "new_accounts_public_key_uri_key",
|
||||
new: "accounts_public_key_uri_key",
|
||||
columns: []string{"public_key_uri"},
|
||||
},
|
||||
} {
|
||||
if _, err := tx.ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? DROP CONSTRAINT IF EXISTS ?",
|
||||
bun.Ident("public.accounts"),
|
||||
bun.Safe(spec.old),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(
|
||||
ctx,
|
||||
"ALTER TABLE ? ADD CONSTRAINT ? UNIQUE(?)",
|
||||
bun.Ident("public.accounts"),
|
||||
bun.Safe(spec.new),
|
||||
bun.Safe(strings.Join(spec.columns, ",")),
|
||||
); 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,26 @@
|
|||
// 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 common
|
||||
|
||||
import "time"
|
||||
|
||||
type Field struct {
|
||||
Name string
|
||||
Value string
|
||||
VerifiedAt time.Time `bun:",nullzero"`
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// 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 (
|
||||
"crypto/rsa"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
bun.BaseModel `bun:"table:new_accounts"`
|
||||
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"`
|
||||
Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
|
||||
Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
|
||||
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
AvatarRemoteURL string `bun:",nullzero"`
|
||||
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
HeaderRemoteURL string `bun:",nullzero"`
|
||||
DisplayName string `bun:",nullzero"`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Fields []*common.Field `bun:",nullzero"`
|
||||
FieldsRaw []*common.Field `bun:",nullzero"`
|
||||
Note string `bun:",nullzero"`
|
||||
NoteRaw string `bun:",nullzero"`
|
||||
MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
|
||||
MovedToURI string `bun:",nullzero"`
|
||||
MoveID string `bun:"type:CHAR(26),nullzero"`
|
||||
Locked *bool `bun:",nullzero,notnull,default:true"`
|
||||
Discoverable *bool `bun:",nullzero,notnull,default:false"`
|
||||
URI string `bun:",nullzero,notnull,unique"`
|
||||
URL string `bun:",nullzero"`
|
||||
InboxURI string `bun:",nullzero"`
|
||||
SharedInboxURI *string `bun:""`
|
||||
OutboxURI string `bun:",nullzero"`
|
||||
FollowingURI string `bun:",nullzero"`
|
||||
FollowersURI string `bun:",nullzero"`
|
||||
FeaturedCollectionURI string `bun:",nullzero"`
|
||||
ActorType AccountActorType `bun:",nullzero,notnull"`
|
||||
PrivateKey *rsa.PrivateKey `bun:""`
|
||||
PublicKey *rsa.PublicKey `bun:",notnull"`
|
||||
PublicKeyURI string `bun:",nullzero,notnull,unique"`
|
||||
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
|
||||
}
|
||||
|
||||
type AccountActorType int16
|
||||
|
||||
const (
|
||||
AccountActorTypeUnknown AccountActorType = 0
|
||||
AccountActorTypeApplication AccountActorType = 1 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
|
||||
AccountActorTypeGroup AccountActorType = 2 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
|
||||
AccountActorTypeOrganization AccountActorType = 3 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
|
||||
AccountActorTypePerson AccountActorType = 4 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
||||
AccountActorTypeService AccountActorType = 5 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
|
||||
)
|
||||
|
||||
func ParseAccountActorType(in string) AccountActorType {
|
||||
switch strings.ToLower(in) {
|
||||
case "application":
|
||||
return AccountActorTypeApplication
|
||||
case "group":
|
||||
return AccountActorTypeGroup
|
||||
case "organization":
|
||||
return AccountActorTypeOrganization
|
||||
case "person":
|
||||
return AccountActorTypePerson
|
||||
case "service":
|
||||
return AccountActorTypeService
|
||||
default:
|
||||
return AccountActorTypeUnknown
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// 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 (
|
||||
"crypto/rsa"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250321131230_relax_account_uri_uniqueness/common"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
bun.BaseModel `bun:"table:accounts"`
|
||||
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"`
|
||||
Username string `bun:",nullzero,notnull,unique:usernamedomain"`
|
||||
Domain string `bun:",nullzero,unique:usernamedomain"`
|
||||
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
AvatarRemoteURL string `bun:",nullzero"`
|
||||
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
HeaderRemoteURL string `bun:",nullzero"`
|
||||
DisplayName string `bun:""`
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
Fields []*common.Field `bun:""`
|
||||
FieldsRaw []*common.Field `bun:""`
|
||||
Note string `bun:""`
|
||||
NoteRaw string `bun:""`
|
||||
Memorial *bool `bun:",default:false"`
|
||||
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
|
||||
MovedToURI string `bun:",nullzero"`
|
||||
MoveID string `bun:"type:CHAR(26),nullzero"`
|
||||
Bot *bool `bun:",default:false"`
|
||||
Locked *bool `bun:",default:true"`
|
||||
Discoverable *bool `bun:",default:false"`
|
||||
URI string `bun:",nullzero,notnull,unique"`
|
||||
URL string `bun:",nullzero,unique"`
|
||||
InboxURI string `bun:",nullzero,unique"`
|
||||
SharedInboxURI *string `bun:""`
|
||||
OutboxURI string `bun:",nullzero,unique"`
|
||||
FollowingURI string `bun:",nullzero,unique"`
|
||||
FollowersURI string `bun:",nullzero,unique"`
|
||||
FeaturedCollectionURI string `bun:",nullzero,unique"`
|
||||
ActorType string `bun:",nullzero,notnull"`
|
||||
PrivateKey *rsa.PrivateKey `bun:""`
|
||||
PublicKey *rsa.PublicKey `bun:",notnull"`
|
||||
PublicKeyURI string `bun:",nullzero,notnull,unique"`
|
||||
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
|
||||
}
|
||||
|
|
@ -31,8 +31,8 @@ type Domain interface {
|
|||
Block/allow storage + retrieval functions.
|
||||
*/
|
||||
|
||||
// CreateDomainAllow puts the given instance-level domain allow into the database.
|
||||
CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error
|
||||
// PutDomainAllow puts the given instance-level domain allow into the database.
|
||||
PutDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error
|
||||
|
||||
// GetDomainAllow returns one instance-level domain allow with the given domain, if it exists.
|
||||
GetDomainAllow(ctx context.Context, domain string) (*gtsmodel.DomainAllow, error)
|
||||
|
|
@ -49,8 +49,8 @@ type Domain interface {
|
|||
// DeleteDomainAllow deletes an instance-level domain allow with the given domain, if it exists.
|
||||
DeleteDomainAllow(ctx context.Context, domain string) error
|
||||
|
||||
// CreateDomainBlock puts the given instance-level domain block into the database.
|
||||
CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error
|
||||
// PutDomainBlock puts the given instance-level domain block into the database.
|
||||
PutDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error
|
||||
|
||||
// GetDomainBlock returns one instance-level domain block with the given domain, if it exists.
|
||||
GetDomainBlock(ctx context.Context, domain string) (*gtsmodel.DomainBlock, error)
|
||||
|
|
|
|||
|
|
@ -29,4 +29,8 @@ var (
|
|||
|
||||
// ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert.
|
||||
ErrAlreadyExists = errors.New("already exists")
|
||||
|
||||
// ErrMultipleEntries is returned when multiple entries
|
||||
// are found in the db when only one entry is sought.
|
||||
ErrMultipleEntries = errors.New("multiple entries")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (s *sender) sendTemplate(template string, subject string, data any, toAddresses ...string) error {
|
||||
|
|
@ -105,7 +106,7 @@ func assembleMessage(mailSubject string, mailBody string, mailFrom string, msgID
|
|||
// msg headers.'
|
||||
msg.WriteString("To: Undisclosed Recipients:;" + CRLF)
|
||||
}
|
||||
msg.WriteString("Date: " + time.Now().Format(time.RFC822Z) + CRLF)
|
||||
msg.WriteString("Date: " + util.FormatRFC2822(time.Now()) + CRLF)
|
||||
msg.WriteString("From: " + mailFrom + CRLF)
|
||||
msg.WriteString("Message-ID: <" + uuid.New().String() + "@" + msgIDHost + ">" + CRLF)
|
||||
msg.WriteString("Subject: " + mailSubject + CRLF)
|
||||
|
|
|
|||
|
|
@ -199,9 +199,11 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
|||
}
|
||||
|
||||
// Dereference the account located at owner URI.
|
||||
// Use exact URI match, not URL match.
|
||||
pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx,
|
||||
requestedUsername,
|
||||
pubKeyAuth.OwnerURI,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
if gtserror.StatusCode(err) == http.StatusGone {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||
"codeberg.org/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
|
|
@ -88,14 +89,30 @@ func accountFresh(
|
|||
return !time.Now().After(staleAt)
|
||||
}
|
||||
|
||||
// GetAccountByURI will attempt to fetch an accounts by its URI, first checking the database. In the case of a newly-met remote model, or a remote model
|
||||
// whose last_fetched date is beyond a certain interval, the account will be dereferenced. In the case of dereferencing, some low-priority account information
|
||||
// may be enqueued for asynchronous fetching, e.g. featured account statuses (pins). An ActivityPub object indicates the account was dereferenced.
|
||||
func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) {
|
||||
// GetAccountByURI will attempt to fetch an accounts by its
|
||||
// URI, first checking the database. In the case of a newly-met
|
||||
// remote model, or a remote model whose last_fetched date is
|
||||
// beyond a certain interval, the account will be dereferenced.
|
||||
// In the case of dereferencing, some low-priority account info
|
||||
// may be enqueued for asynchronous fetching, e.g. pinned statuses.
|
||||
// An ActivityPub object indicates the account was dereferenced.
|
||||
//
|
||||
// if tryURL is true, then the database will also check for a *single*
|
||||
// account where uri == account.url, not just uri == account.uri.
|
||||
// Because url does not guarantee uniqueness, you should only set
|
||||
// tryURL to true when doing searches set in motion by a user,
|
||||
// ie., when it's not important that an exact account is returned.
|
||||
func (d *Dereferencer) GetAccountByURI(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
uri *url.URL,
|
||||
tryURL bool,
|
||||
) (*gtsmodel.Account, ap.Accountable, error) {
|
||||
// Fetch and dereference account if necessary.
|
||||
account, accountable, err := d.getAccountByURI(ctx,
|
||||
requestUser,
|
||||
uri,
|
||||
tryURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -117,8 +134,15 @@ func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string,
|
|||
return account, accountable, nil
|
||||
}
|
||||
|
||||
// getAccountByURI is a package internal form of .GetAccountByURI() that doesn't bother dereferencing featured posts on update.
|
||||
func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, ap.Accountable, error) {
|
||||
// getAccountByURI is a package internal form of
|
||||
// .GetAccountByURI() that doesn't bother dereferencing
|
||||
// featured posts on update.
|
||||
func (d *Dereferencer) getAccountByURI(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
uri *url.URL,
|
||||
tryURL bool,
|
||||
) (*gtsmodel.Account, ap.Accountable, error) {
|
||||
var (
|
||||
account *gtsmodel.Account
|
||||
uriStr = uri.String()
|
||||
|
|
@ -126,9 +150,8 @@ func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string,
|
|||
)
|
||||
|
||||
// Search the database for existing account with URI.
|
||||
// URI is unique so if we get a hit it's that account for sure.
|
||||
account, err = d.state.DB.GetAccountByURI(
|
||||
// request a barebones object, it may be in the
|
||||
// db but with related models not yet dereferenced.
|
||||
gtscontext.SetBarebones(ctx),
|
||||
uriStr,
|
||||
)
|
||||
|
|
@ -136,13 +159,20 @@ func (d *Dereferencer) getAccountByURI(ctx context.Context, requestUser string,
|
|||
return nil, nil, gtserror.Newf("error checking database for account %s by uri: %w", uriStr, err)
|
||||
}
|
||||
|
||||
if account == nil {
|
||||
// Else, search the database for existing by URL.
|
||||
account, err = d.state.DB.GetAccountByURL(
|
||||
if account == nil && tryURL {
|
||||
// Else if we're permitted, search the database for *ONE*
|
||||
// account with this URL. This can return multiple hits
|
||||
// so check for ErrMultipleEntries. If we get exactly one
|
||||
// hit it's *probably* the account we're looking for.
|
||||
account, err = d.state.DB.GetOneAccountByURL(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
uriStr,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
if err != nil && !errorsv2.IsV2(
|
||||
err,
|
||||
db.ErrNoEntries,
|
||||
db.ErrMultipleEntries,
|
||||
) {
|
||||
return nil, nil, gtserror.Newf("error checking database for account %s by url: %w", uriStr, err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
groupURL,
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(group)
|
||||
|
|
@ -67,7 +68,7 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
|
|||
dbGroup, err := suite.db.GetAccountByURI(context.Background(), group.URI)
|
||||
suite.NoError(err)
|
||||
suite.Equal(group.ID, dbGroup.ID)
|
||||
suite.Equal(ap.ActorGroup, dbGroup.ActorType)
|
||||
suite.Equal(ap.ActorGroup, dbGroup.ActorType.String())
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestDereferenceService() {
|
||||
|
|
@ -78,6 +79,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
serviceURL,
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(service)
|
||||
|
|
@ -91,7 +93,7 @@ func (suite *AccountTestSuite) TestDereferenceService() {
|
|||
dbService, err := suite.db.GetAccountByURI(context.Background(), service.URI)
|
||||
suite.NoError(err)
|
||||
suite.Equal(service.ID, dbService.ID)
|
||||
suite.Equal(ap.ActorService, dbService.ActorType)
|
||||
suite.Equal(ap.ActorService, dbService.ActorType.String())
|
||||
suite.Equal("example.org", dbService.Domain)
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +112,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURL() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse(targetAccount.URI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(fetchedAccount)
|
||||
|
|
@ -129,6 +132,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURLNoSharedInb
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse(targetAccount.URI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(fetchedAccount)
|
||||
|
|
@ -143,6 +147,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsername() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse(targetAccount.URI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(fetchedAccount)
|
||||
|
|
@ -157,6 +162,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsernameDomain() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse(targetAccount.URI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(fetchedAccount)
|
||||
|
|
@ -213,6 +219,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"),
|
||||
false,
|
||||
)
|
||||
suite.True(gtserror.IsUnretrievable(err))
|
||||
suite.EqualError(err, db.ErrNoEntries.Error())
|
||||
|
|
@ -265,7 +272,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountByRedirect() {
|
|||
uri := testrig.URLMustParse("https://this-will-be-redirected.butts/")
|
||||
|
||||
// Try dereference the test URI, since it correctly redirects to us it should return our account.
|
||||
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
|
||||
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri, false)
|
||||
suite.NoError(err)
|
||||
suite.Nil(accountable)
|
||||
suite.NotNil(account)
|
||||
|
|
@ -318,7 +325,7 @@ func (suite *AccountTestSuite) TestDereferenceMasqueradingLocalAccount() {
|
|||
)
|
||||
|
||||
// Try dereference the test URI, since it correctly redirects to us it should return our account.
|
||||
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri)
|
||||
account, accountable, err := suite.dereferencer.GetAccountByURI(ctx, fetchingAccount.Username, uri, false)
|
||||
suite.NotNil(err)
|
||||
suite.Nil(account)
|
||||
suite.Nil(accountable)
|
||||
|
|
@ -341,6 +348,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithNonMatchingURI()
|
|||
context.Background(),
|
||||
fetchingAccount.Username,
|
||||
testrig.URLMustParse(remoteAltURI),
|
||||
false,
|
||||
)
|
||||
suite.Equal(err.Error(), fmt.Sprintf("enrichAccount: account uri %s does not match %s", remoteURI, remoteAltURI))
|
||||
suite.Nil(fetchedAccount)
|
||||
|
|
@ -357,6 +365,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithUnexpectedKeyChan
|
|||
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
|
||||
fetchingAcc.Username,
|
||||
testrig.URLMustParse(remoteURI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(remoteAcc)
|
||||
|
|
@ -395,6 +404,7 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithExpectedKeyChange
|
|||
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
|
||||
fetchingAcc.Username,
|
||||
testrig.URLMustParse(remoteURI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(remoteAcc)
|
||||
|
|
@ -436,6 +446,7 @@ func (suite *AccountTestSuite) TestRefreshFederatedRemoteAccountWithKeyChange()
|
|||
remoteAcc, _, err := suite.dereferencer.GetAccountByURI(ctx,
|
||||
fetchingAcc.Username,
|
||||
testrig.URLMustParse(remoteURI),
|
||||
false,
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(remoteAcc)
|
||||
|
|
|
|||
|
|
@ -454,7 +454,8 @@ func (d *Dereferencer) enrichStatus(
|
|||
|
||||
// Ensure we have the author account of the status dereferenced (+ up-to-date). If this is a new status
|
||||
// (i.e. status.AccountID == "") then any error here is irrecoverable. status.AccountID must ALWAYS be set.
|
||||
if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo); err != nil && status.AccountID == "" {
|
||||
// We want the exact URI match here as well, not the imprecise URL match.
|
||||
if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo, false); err != nil && status.AccountID == "" {
|
||||
|
||||
// Note that we specifically DO NOT wrap the error, instead collapsing it as string.
|
||||
// Errors fetching an account do not necessarily relate to dereferencing the status.
|
||||
|
|
@ -671,7 +672,7 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
|
||||
// Search existing status for a mention already stored,
|
||||
// else ensure new mention's target account is populated.
|
||||
mention, alreadyExists, err = d.getPopulatedMention(ctx,
|
||||
mention, alreadyExists, err = d.populateMentionTarget(ctx,
|
||||
requestUser,
|
||||
existing,
|
||||
mention,
|
||||
|
|
@ -1290,7 +1291,7 @@ func (d *Dereferencer) handleStatusEdit(
|
|||
return cols, nil
|
||||
}
|
||||
|
||||
// getPopulatedMention tries to populate the given
|
||||
// populateMentionTarget tries to populate the given
|
||||
// mention with the correct TargetAccount and (if not
|
||||
// yet set) TargetAccountURI, returning the populated
|
||||
// mention.
|
||||
|
|
@ -1302,7 +1303,13 @@ func (d *Dereferencer) handleStatusEdit(
|
|||
// Otherwise, this function will try to parse first
|
||||
// the Href of the mention, and then the namestring,
|
||||
// to see who it targets, and go fetch that account.
|
||||
func (d *Dereferencer) getPopulatedMention(
|
||||
//
|
||||
// Note: Ordinarily it would make sense to try the
|
||||
// namestring first, as it definitely can't be a URL
|
||||
// rather than a URI, but because some remotes do
|
||||
// silly things like only provide `@username` instead
|
||||
// of `@username@domain`, we try by URI first.
|
||||
func (d *Dereferencer) populateMentionTarget(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
|
|
@ -1312,8 +1319,9 @@ func (d *Dereferencer) getPopulatedMention(
|
|||
bool, // True if mention already exists in the DB.
|
||||
error,
|
||||
) {
|
||||
// Mentions can be created using Name or Href.
|
||||
// Prefer Href (TargetAccountURI), fall back to Name.
|
||||
// Mentions can be created using `name` or `href`.
|
||||
//
|
||||
// Prefer `href` (TargetAccountURI), fall back to Name.
|
||||
if mention.TargetAccountURI != "" {
|
||||
|
||||
// Look for existing mention with target account's URI, if so use this.
|
||||
|
|
@ -1323,19 +1331,24 @@ func (d *Dereferencer) getPopulatedMention(
|
|||
}
|
||||
|
||||
// Ensure that mention account URI is parseable.
|
||||
accountURI, err := url.Parse(mention.TargetAccountURI)
|
||||
targetAccountURI, err := url.Parse(mention.TargetAccountURI)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Ensure we have account of the mention target dereferenced.
|
||||
// Ensure we have the account of
|
||||
// the mention target dereferenced.
|
||||
//
|
||||
// Use exact URI match only, not URL,
|
||||
// as we want to be precise here.
|
||||
mention.TargetAccount, _, err = d.getAccountByURI(ctx,
|
||||
requestUser,
|
||||
accountURI,
|
||||
targetAccountURI,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
|
||||
err := gtserror.Newf("failed to dereference account %s: %w", targetAccountURI, err)
|
||||
return nil, false, err
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1353,17 +1366,32 @@ func (d *Dereferencer) getPopulatedMention(
|
|||
return existingMention, true, nil
|
||||
}
|
||||
|
||||
// Ensure we have the account of the mention target dereferenced.
|
||||
// Ensure we have the account of
|
||||
// the mention target dereferenced.
|
||||
//
|
||||
// This might fail if the remote does
|
||||
// something silly like only setting
|
||||
// `@username` and not `@username@domain`.
|
||||
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx,
|
||||
requestUser,
|
||||
username,
|
||||
domain,
|
||||
)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if mention.TargetAccount == nil {
|
||||
// Probably failed for abovementioned
|
||||
// silly reason. Nothing we can do about it.
|
||||
err := gtserror.Newf(
|
||||
"failed to populate mention target account (badly formatted namestring?) %s: %w",
|
||||
mention.NameString, err,
|
||||
)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Look for existing mention with target account's URI, if so use this.
|
||||
existingMention, ok = existing.GetMentionByTargetURI(mention.TargetAccountURI)
|
||||
if ok && existingMention.ID != "" {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import (
|
|||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
|
||||
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
||||
acct, err := f.state.DB.GetAccountByURI(ctx, actorIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import (
|
|||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) {
|
||||
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
||||
acct, err := f.state.DB.GetAccountByURI(ctx, actorIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ func (f *federatingDB) SetOutbox(ctx context.Context, outbox vocab.ActivityStrea
|
|||
return nil
|
||||
}
|
||||
|
||||
// OutboxForInbox fetches the corresponding actor's outbox IRI for the
|
||||
// OutboxForInbox fetches the corresponding local actor's outbox IRI for the
|
||||
// actor's inbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
func (f *federatingDB) OutboxForInbox(ctx context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) {
|
||||
acct, err := f.getAccountForIRI(ctx, inboxIRI)
|
||||
acct, err := f.state.DB.GetOneAccountByInboxURI(ctx, inboxIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import (
|
|||
"codeberg.org/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
|
|
@ -126,83 +125,30 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL,
|
|||
return url.Parse(fmt.Sprintf("%s://%s/%s", config.GetProtocol(), config.GetHost(), newID))
|
||||
}
|
||||
|
||||
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
|
||||
// ActorForOutbox fetches the local actor's IRI for the given outbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
func (f *federatingDB) ActorForOutbox(ctx context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) {
|
||||
acct, err := f.getAccountForIRI(ctx, outboxIRI)
|
||||
acct, err := f.state.DB.GetOneAccountByOutboxURI(ctx, outboxIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url.Parse(acct.URI)
|
||||
}
|
||||
|
||||
// ActorForInbox fetches the actor's IRI for the given outbox IRI.
|
||||
// ActorForInbox fetches the local actor's IRI for the given inbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
func (f *federatingDB) ActorForInbox(ctx context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) {
|
||||
acct, err := f.getAccountForIRI(ctx, inboxIRI)
|
||||
acct, err := f.state.DB.GetOneAccountByInboxURI(ctx, inboxIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url.Parse(acct.URI)
|
||||
}
|
||||
|
||||
// getAccountForIRI returns the account that corresponds to or owns the given IRI.
|
||||
func (f *federatingDB) getAccountForIRI(ctx context.Context, iri *url.URL) (*gtsmodel.Account, error) {
|
||||
var (
|
||||
acct *gtsmodel.Account
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case uris.IsUserPath(iri):
|
||||
if acct, err = f.state.DB.GetAccountByURI(ctx, iri.String()); err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, fmt.Errorf("no actor found that corresponds to uri %s", iri.String())
|
||||
}
|
||||
return nil, fmt.Errorf("db error searching for actor with uri %s", iri.String())
|
||||
}
|
||||
return acct, nil
|
||||
case uris.IsInboxPath(iri):
|
||||
if acct, err = f.state.DB.GetAccountByInboxURI(ctx, iri.String()); err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", iri.String())
|
||||
}
|
||||
return nil, fmt.Errorf("db error searching for actor with inbox %s", iri.String())
|
||||
}
|
||||
return acct, nil
|
||||
case uris.IsOutboxPath(iri):
|
||||
if acct, err = f.state.DB.GetAccountByOutboxURI(ctx, iri.String()); err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, fmt.Errorf("no actor found that corresponds to outbox %s", iri.String())
|
||||
}
|
||||
return nil, fmt.Errorf("db error searching for actor with outbox %s", iri.String())
|
||||
}
|
||||
return acct, nil
|
||||
case uris.IsFollowersPath(iri):
|
||||
if acct, err = f.state.DB.GetAccountByFollowersURI(ctx, iri.String()); err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, fmt.Errorf("no actor found that corresponds to followers_uri %s", iri.String())
|
||||
}
|
||||
return nil, fmt.Errorf("db error searching for actor with followers_uri %s", iri.String())
|
||||
}
|
||||
return acct, nil
|
||||
case uris.IsFollowingPath(iri):
|
||||
if acct, err = f.state.DB.GetAccountByFollowingURI(ctx, iri.String()); err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, fmt.Errorf("no actor found that corresponds to following_uri %s", iri.String())
|
||||
}
|
||||
return nil, fmt.Errorf("db error searching for actor with following_uri %s", iri.String())
|
||||
}
|
||||
return acct, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("getActorForIRI: iri %s not recognised", iri)
|
||||
}
|
||||
}
|
||||
|
||||
// collectFollows takes a slice of iris and converts them into ActivityStreamsCollection of IRIs.
|
||||
func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) {
|
||||
func (f *federatingDB) collectIRIs(_ context.Context, iris []*url.URL) (vocab.ActivityStreamsCollection, error) {
|
||||
collection := streams.NewActivityStreamsCollection()
|
||||
items := streams.NewActivityStreamsItemsProperty()
|
||||
for _, i := range iris {
|
||||
|
|
|
|||
|
|
@ -31,57 +31,247 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// Account represents either a local or a remote fediverse
|
||||
// account, gotosocial or otherwise (mastodon, pleroma, etc).
|
||||
// Account represents either a local or a remote ActivityPub actor.
|
||||
// https://www.w3.org/TR/activitypub/#actor-objects
|
||||
type Account 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 was last updated.
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched.
|
||||
Username string `bun:",nullzero,notnull,unique:usernamedomain"` // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other
|
||||
Domain string `bun:",nullzero,unique:usernamedomain"` // Domain of the account, will be null if this is a local account, otherwise something like ``example.org``. Should be unique with username.
|
||||
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
|
||||
AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID
|
||||
AvatarRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched?
|
||||
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present
|
||||
HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID
|
||||
HeaderRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched?
|
||||
DisplayName string `bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc
|
||||
Emojis []*Emoji `bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
Fields []*Field `bun:""` // A slice of of fields that this account has added to their profile.
|
||||
FieldsRaw []*Field `bun:""` // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target
|
||||
Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
|
||||
NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
|
||||
Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
|
||||
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs.
|
||||
AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db).
|
||||
MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this.
|
||||
MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db).
|
||||
MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin.
|
||||
Move *Move `bun:"-"` // Move corresponding to MoveID, if set.
|
||||
Bot *bool `bun:",default:false"` // Does this account identify itself as a bot?
|
||||
Locked *bool `bun:",default:true"` // Does this account need an approval for new followers?
|
||||
Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory?
|
||||
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
|
||||
URL string `bun:",nullzero,unique"` // Web URL for this account's profile
|
||||
InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
|
||||
SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string.
|
||||
OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox
|
||||
FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account
|
||||
FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
|
||||
FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
|
||||
ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
|
||||
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts
|
||||
PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts
|
||||
PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
|
||||
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts.
|
||||
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
|
||||
SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
|
||||
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
|
||||
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
|
||||
Settings *AccountSettings `bun:"-"` // gtsmodel.AccountSettings for this account.
|
||||
Stats *AccountStats `bun:"-"` // gtsmodel.AccountStats for this account.
|
||||
// Database ID of the account.
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
|
||||
|
||||
// Datetime when the account was created.
|
||||
// Corresponds to ActivityStreams `published` prop.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
|
||||
// Datetime when was the account was last updated,
|
||||
// ie., when the actor last sent out an Update
|
||||
// activity, or if never, when it was `published`.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
|
||||
|
||||
// Datetime when the account was last fetched /
|
||||
// dereferenced by this GoToSocial instance.
|
||||
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// Username of the account.
|
||||
//
|
||||
// Corresponds to AS `preferredUsername` prop, which gives
|
||||
// no uniqueness guarantee. However, we do enforce uniqueness
|
||||
// for it as, in practice, it always is and we rely on this.
|
||||
Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
|
||||
|
||||
// Domain of the account, discovered via webfinger.
|
||||
//
|
||||
// Null if this is a local account, otherwise
|
||||
// something like `example.org`.
|
||||
Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
|
||||
|
||||
// Database ID of the account's avatar MediaAttachment, if set.
|
||||
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
|
||||
// MediaAttachment corresponding to AvatarMediaAttachmentID.
|
||||
AvatarMediaAttachment *MediaAttachment `bun:"-"`
|
||||
|
||||
// URL of the avatar media.
|
||||
//
|
||||
// Null for local accounts.
|
||||
AvatarRemoteURL string `bun:",nullzero"`
|
||||
|
||||
// Database ID of the account's header MediaAttachment, if set.
|
||||
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
|
||||
|
||||
// MediaAttachment corresponding to HeaderMediaAttachmentID.
|
||||
HeaderMediaAttachment *MediaAttachment `bun:"-"`
|
||||
|
||||
// URL of the header media.
|
||||
//
|
||||
// Null for local accounts.
|
||||
HeaderRemoteURL string `bun:",nullzero"`
|
||||
|
||||
// Display name for this account, if set.
|
||||
//
|
||||
// Corresponds to the ActivityStreams `name` property.
|
||||
//
|
||||
// If null, fall back to username for display purposes.
|
||||
DisplayName string `bun:",nullzero"`
|
||||
|
||||
// Database IDs of any emojis used in
|
||||
// this account's bio, display name, etc
|
||||
EmojiIDs []string `bun:"emojis,array"`
|
||||
|
||||
// Emojis corresponding to EmojiIDs.
|
||||
Emojis []*Emoji `bun:"-"`
|
||||
|
||||
// A slice of of key/value fields that
|
||||
// this account has added to their profile.
|
||||
//
|
||||
// Corresponds to schema.org PropertyValue types in `attachments`.
|
||||
Fields []*Field `bun:",nullzero"`
|
||||
|
||||
// The raw (unparsed) content of fields that this
|
||||
// account has added to their profile, before
|
||||
// conversion to HTML.
|
||||
//
|
||||
// Only set for local accounts.
|
||||
FieldsRaw []*Field `bun:",nullzero"`
|
||||
|
||||
// A note that this account has on their profile
|
||||
// (ie., the account's bio/description of themselves).
|
||||
//
|
||||
// Corresponds to the ActivityStreams `summary` property.
|
||||
Note string `bun:",nullzero"`
|
||||
|
||||
// The raw (unparsed) version of Note, before conversion to HTML.
|
||||
//
|
||||
// Only set for local accounts.
|
||||
NoteRaw string `bun:",nullzero"`
|
||||
|
||||
// ActivityPub URI/IDs by which this account is also known.
|
||||
//
|
||||
// Corresponds to the ActivityStreams `alsoKnownAs` property.
|
||||
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
|
||||
|
||||
// Accounts matching AlsoKnownAsURIs.
|
||||
AlsoKnownAs []*Account `bun:"-"`
|
||||
|
||||
// URI/ID to which the account has (or claims to have) moved.
|
||||
//
|
||||
// Corresponds to the ActivityStreams `movedTo` property.
|
||||
//
|
||||
// Even if this field is set the move may not yet have been
|
||||
// processed. Check `move` for this.
|
||||
MovedToURI string `bun:",nullzero"`
|
||||
|
||||
// Account matching MovedToURI.
|
||||
MovedTo *Account `bun:"-"`
|
||||
|
||||
// ID of a Move in the database for this account.
|
||||
// Only set if we received or created a Move activity
|
||||
// for which this account URI was the origin.
|
||||
MoveID string `bun:"type:CHAR(26),nullzero"`
|
||||
|
||||
// Move corresponding to MoveID, if set.
|
||||
Move *Move `bun:"-"`
|
||||
|
||||
// True if account requires manual approval of Follows.
|
||||
//
|
||||
// Corresponds to AS `manuallyApprovesFollowers` prop.
|
||||
Locked *bool `bun:",nullzero,notnull,default:true"`
|
||||
|
||||
// True if account has opted in to being shown in
|
||||
// directories and exposed to search engines.
|
||||
//
|
||||
// Corresponds to the toot `discoverable` property.
|
||||
Discoverable *bool `bun:",nullzero,notnull,default:false"`
|
||||
|
||||
// ActivityPub URI/ID for this account.
|
||||
//
|
||||
// Must be set, must be unique.
|
||||
URI string `bun:",nullzero,notnull,unique"`
|
||||
|
||||
// URL at which a web representation of this
|
||||
// account should be available, if set.
|
||||
//
|
||||
// Corresponds to ActivityStreams `url` prop.
|
||||
URL string `bun:",nullzero"`
|
||||
|
||||
// URI of the actor's inbox.
|
||||
//
|
||||
// Corresponds to ActivityPub `inbox` property.
|
||||
//
|
||||
// According to AP this MUST be set, but some
|
||||
// implementations don't set it for service actors.
|
||||
InboxURI string `bun:",nullzero"`
|
||||
|
||||
// URI/ID of this account's sharedInbox, if set.
|
||||
//
|
||||
// Corresponds to ActivityPub `endpoints.sharedInbox`.
|
||||
//
|
||||
// Gotcha warning: this is a string pointer because
|
||||
// it has three possible states:
|
||||
//
|
||||
// 1. null: We don't know (yet) if actor has a shared inbox.
|
||||
// 2. empty: We know it doesn't have a shared inbox.
|
||||
// 3. not empty: We know it does have a shared inbox.
|
||||
SharedInboxURI *string `bun:""`
|
||||
|
||||
// URI/ID of the actor's outbox.
|
||||
//
|
||||
// Corresponds to ActivityPub `outbox` property.
|
||||
//
|
||||
// According to AP this MUST be set, but some
|
||||
// implementations don't set it for service actors.
|
||||
OutboxURI string `bun:",nullzero"`
|
||||
|
||||
// URI/ID of the actor's following collection.
|
||||
//
|
||||
// Corresponds to ActivityPub `following` property.
|
||||
//
|
||||
// According to AP this SHOULD be set.
|
||||
FollowingURI string `bun:",nullzero"`
|
||||
|
||||
// URI/ID of the actor's followers collection.
|
||||
//
|
||||
// Corresponds to ActivityPub `followers` property.
|
||||
//
|
||||
// According to AP this SHOULD be set.
|
||||
FollowersURI string `bun:",nullzero"`
|
||||
|
||||
// URI/ID of the actor's featured collection.
|
||||
//
|
||||
// Corresponds to the Toot `featured` property.
|
||||
FeaturedCollectionURI string `bun:",nullzero"`
|
||||
|
||||
// ActivityStreams type of the actor.
|
||||
//
|
||||
// Application, Group, Organization, Person, or Service.
|
||||
ActorType AccountActorType `bun:",nullzero,notnull"`
|
||||
|
||||
// Private key for signing http requests.
|
||||
//
|
||||
// Only defined for local accounts
|
||||
PrivateKey *rsa.PrivateKey `bun:""`
|
||||
|
||||
// Public key for authorizing signed http requests.
|
||||
//
|
||||
// Defined for both local and remote accounts
|
||||
PublicKey *rsa.PublicKey `bun:",notnull"`
|
||||
|
||||
// Dereferenceable location of this actor's public key.
|
||||
//
|
||||
// Corresponds to https://w3id.org/security/v1 `publicKey.id`.
|
||||
PublicKeyURI string `bun:",nullzero,notnull,unique"`
|
||||
|
||||
// Datetime at which public key will expire/has expired,
|
||||
// and should be fetched again as appropriate.
|
||||
//
|
||||
// Only ever set for remote accounts.
|
||||
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// Datetime at which account was marked as a "memorial",
|
||||
// ie., user owning the account has passed away.
|
||||
MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// Datetime at which account was set to
|
||||
// have all its media shown as sensitive.
|
||||
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// Datetime at which account was silenced.
|
||||
SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// Datetime at which account was suspended.
|
||||
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
|
||||
|
||||
// ID of the database entry that caused this account to
|
||||
// be suspended. Can be an account ID or a domain block ID.
|
||||
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
|
||||
|
||||
// gtsmodel.AccountSettings for this account.
|
||||
//
|
||||
// Local, non-instance-actor accounts only.
|
||||
Settings *AccountSettings `bun:"-"`
|
||||
|
||||
// gtsmodel.AccountStats for this account.
|
||||
//
|
||||
// Local accounts only.
|
||||
Stats *AccountStats `bun:"-"`
|
||||
}
|
||||
|
||||
// UsernameDomain returns account @username@domain (missing domain if local).
|
||||
|
|
@ -215,6 +405,59 @@ type Field struct {
|
|||
VerifiedAt time.Time `bun:",nullzero"` // This field was verified at (optional).
|
||||
}
|
||||
|
||||
// AccountActorType is the ActivityStreams type of an actor.
|
||||
type AccountActorType enumType
|
||||
|
||||
const (
|
||||
AccountActorTypeUnknown AccountActorType = 0
|
||||
AccountActorTypeApplication AccountActorType = 1 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
|
||||
AccountActorTypeGroup AccountActorType = 2 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
|
||||
AccountActorTypeOrganization AccountActorType = 3 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
|
||||
AccountActorTypePerson AccountActorType = 4 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
||||
AccountActorTypeService AccountActorType = 5 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
|
||||
)
|
||||
|
||||
// String returns a stringified form of AccountActorType.
|
||||
func (t AccountActorType) String() string {
|
||||
switch t {
|
||||
case AccountActorTypeApplication:
|
||||
return "Application"
|
||||
case AccountActorTypeGroup:
|
||||
return "Group"
|
||||
case AccountActorTypeOrganization:
|
||||
return "Organization"
|
||||
case AccountActorTypePerson:
|
||||
return "Person"
|
||||
case AccountActorTypeService:
|
||||
return "Service"
|
||||
default:
|
||||
panic("invalid notification type")
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAccountActorType returns an
|
||||
// actor type from the given value.
|
||||
func ParseAccountActorType(in string) AccountActorType {
|
||||
switch strings.ToLower(in) {
|
||||
case "application":
|
||||
return AccountActorTypeApplication
|
||||
case "group":
|
||||
return AccountActorTypeGroup
|
||||
case "organization":
|
||||
return AccountActorTypeOrganization
|
||||
case "person":
|
||||
return AccountActorTypePerson
|
||||
case "service":
|
||||
return AccountActorTypeService
|
||||
default:
|
||||
return AccountActorTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (t AccountActorType) IsBot() bool {
|
||||
return t == AccountActorTypeApplication || t == AccountActorTypeService
|
||||
}
|
||||
|
||||
// Relationship describes a requester's relationship with another account.
|
||||
type Relationship struct {
|
||||
ID string // The account id.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ type DomainAllow struct {
|
|||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Domain string `bun:",nullzero,notnull"` // domain to allow. Eg. 'whatever.com'
|
||||
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this allow
|
||||
CreatedByAccount *Account `bun:"rel:belongs-to"` // Account corresponding to createdByAccountID
|
||||
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID
|
||||
PrivateComment string `bun:""` // Private comment on this allow, viewable to admins
|
||||
PublicComment string `bun:""` // Public comment on this allow, viewable (optionally) by everyone
|
||||
Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ type DomainBlock struct {
|
|||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Domain string `bun:",nullzero,notnull"` // domain to block. Eg. 'whatever.com'
|
||||
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this block
|
||||
CreatedByAccount *Account `bun:"rel:belongs-to"` // Account corresponding to createdByAccountID
|
||||
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID
|
||||
PrivateComment string `bun:""` // Private comment on this block, viewable to admins
|
||||
PublicComment string `bun:""` // Public comment on this block, viewable (optionally) by everyone
|
||||
Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly
|
||||
|
|
|
|||
|
|
@ -107,9 +107,14 @@ func (p *Processor) Alias(
|
|||
}
|
||||
|
||||
// Ensure we have account dereferenced.
|
||||
//
|
||||
// As this comes from user input, allow checking
|
||||
// by URL to make things easier, not just to an
|
||||
// exact AP URI (which a user might not even know).
|
||||
targetAccount, _, err := p.federator.GetAccountByURI(ctx,
|
||||
account.Username,
|
||||
newAKA.uri,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
|
|||
account.Fields = nil
|
||||
account.Note = ""
|
||||
account.NoteRaw = ""
|
||||
account.Memorial = util.Ptr(false)
|
||||
account.MemorializedAt = never
|
||||
account.AlsoKnownAsURIs = nil
|
||||
account.MovedToURI = ""
|
||||
account.Discoverable = util.Ptr(false)
|
||||
|
|
@ -546,7 +546,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
|
|||
"fields",
|
||||
"note",
|
||||
"note_raw",
|
||||
"memorial",
|
||||
"memorialized_at",
|
||||
"also_known_as_uris",
|
||||
"moved_to_uri",
|
||||
"discoverable",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
|
|||
suite.Nil(updatedAccount.Fields)
|
||||
suite.Zero(updatedAccount.Note)
|
||||
suite.Zero(updatedAccount.NoteRaw)
|
||||
suite.False(*updatedAccount.Memorial)
|
||||
suite.Zero(updatedAccount.MemorializedAt)
|
||||
suite.Empty(updatedAccount.AlsoKnownAsURIs)
|
||||
suite.False(*updatedAccount.Discoverable)
|
||||
suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute)
|
||||
|
|
|
|||
|
|
@ -66,10 +66,13 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
|||
|
||||
// Perform a last-minute fetch of target account to
|
||||
// ensure remote account header / avatar is cached.
|
||||
//
|
||||
// Match by URI only.
|
||||
latest, _, err := p.federator.GetAccountByURI(
|
||||
gtscontext.SetFastFail(ctx),
|
||||
requestingAccount.Username,
|
||||
targetAccountURI,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error fetching latest target account: %v", err)
|
||||
|
|
|
|||
|
|
@ -119,11 +119,15 @@ func (p *Processor) MoveSelf(
|
|||
unlock := p.state.ProcessingLocks.Lock(lockKey)
|
||||
defer unlock()
|
||||
|
||||
// Ensure we have a valid, up-to-date representation of the target account.
|
||||
// Ensure we have a valid, up-to-date
|
||||
// representation of the target account.
|
||||
//
|
||||
// Match by uri only.
|
||||
targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
|
||||
ctx,
|
||||
originAcct.Username,
|
||||
targetAcctURI,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
const text = "error dereferencing moved_to_uri"
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
|||
}
|
||||
|
||||
if form.Bot != nil {
|
||||
account.Bot = form.Bot
|
||||
acctColumns = append(acctColumns, "bot")
|
||||
account.ActorType = gtsmodel.AccountActorTypeService
|
||||
acctColumns = append(acctColumns, "actor_type")
|
||||
}
|
||||
|
||||
if form.Locked != nil {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ func (p *Processor) createDomainAllow(
|
|||
}
|
||||
|
||||
// Insert the new allow into the database.
|
||||
if err := p.state.DB.CreateDomainAllow(ctx, domainAllow); err != nil {
|
||||
if err := p.state.DB.PutDomainAllow(ctx, domainAllow); err != nil {
|
||||
err = gtserror.Newf("db error putting domain allow %s: %w", domain, err)
|
||||
return nil, "", gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -92,6 +92,54 @@ func (p *Processor) createDomainAllow(
|
|||
return apiDomainAllow, action.ID, nil
|
||||
}
|
||||
|
||||
func (p *Processor) updateDomainAllow(
|
||||
ctx context.Context,
|
||||
domainAllowID string,
|
||||
obfuscate *bool,
|
||||
publicComment *string,
|
||||
privateComment *string,
|
||||
subscriptionID *string,
|
||||
) (*apimodel.DomainPermission, gtserror.WithCode) {
|
||||
domainAllow, err := p.state.DB.GetDomainAllowByID(ctx, domainAllowID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real error.
|
||||
err = gtserror.Newf("db error getting domain allow: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// There are just no entries for this ID.
|
||||
err = fmt.Errorf("no domain allow entry exists with ID %s", domainAllowID)
|
||||
return nil, gtserror.NewErrorNotFound(err, err.Error())
|
||||
}
|
||||
|
||||
var columns []string
|
||||
if obfuscate != nil {
|
||||
domainAllow.Obfuscate = obfuscate
|
||||
columns = append(columns, "obfuscate")
|
||||
}
|
||||
if publicComment != nil {
|
||||
domainAllow.PublicComment = *publicComment
|
||||
columns = append(columns, "public_comment")
|
||||
}
|
||||
if privateComment != nil {
|
||||
domainAllow.PrivateComment = *privateComment
|
||||
columns = append(columns, "private_comment")
|
||||
}
|
||||
if subscriptionID != nil {
|
||||
domainAllow.SubscriptionID = *subscriptionID
|
||||
columns = append(columns, "subscription_id")
|
||||
}
|
||||
|
||||
// Update the domain allow.
|
||||
if err := p.state.DB.UpdateDomainAllow(ctx, domainAllow, columns...); err != nil {
|
||||
err = gtserror.Newf("db error updating domain allow: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.apiDomainPerm(ctx, domainAllow, false)
|
||||
}
|
||||
|
||||
func (p *Processor) deleteDomainAllow(
|
||||
ctx context.Context,
|
||||
adminAcct *gtsmodel.Account,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ func (p *Processor) createDomainBlock(
|
|||
}
|
||||
|
||||
// Insert the new block into the database.
|
||||
if err := p.state.DB.CreateDomainBlock(ctx, domainBlock); err != nil {
|
||||
if err := p.state.DB.PutDomainBlock(ctx, domainBlock); err != nil {
|
||||
err = gtserror.Newf("db error putting domain block %s: %w", domain, err)
|
||||
return nil, "", gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -93,6 +93,54 @@ func (p *Processor) createDomainBlock(
|
|||
return apiDomainBlock, action.ID, nil
|
||||
}
|
||||
|
||||
func (p *Processor) updateDomainBlock(
|
||||
ctx context.Context,
|
||||
domainBlockID string,
|
||||
obfuscate *bool,
|
||||
publicComment *string,
|
||||
privateComment *string,
|
||||
subscriptionID *string,
|
||||
) (*apimodel.DomainPermission, gtserror.WithCode) {
|
||||
domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real error.
|
||||
err = gtserror.Newf("db error getting domain block: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// There are just no entries for this ID.
|
||||
err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID)
|
||||
return nil, gtserror.NewErrorNotFound(err, err.Error())
|
||||
}
|
||||
|
||||
var columns []string
|
||||
if obfuscate != nil {
|
||||
domainBlock.Obfuscate = obfuscate
|
||||
columns = append(columns, "obfuscate")
|
||||
}
|
||||
if publicComment != nil {
|
||||
domainBlock.PublicComment = *publicComment
|
||||
columns = append(columns, "public_comment")
|
||||
}
|
||||
if privateComment != nil {
|
||||
domainBlock.PrivateComment = *privateComment
|
||||
columns = append(columns, "private_comment")
|
||||
}
|
||||
if subscriptionID != nil {
|
||||
domainBlock.SubscriptionID = *subscriptionID
|
||||
columns = append(columns, "subscription_id")
|
||||
}
|
||||
|
||||
// Update the domain block.
|
||||
if err := p.state.DB.UpdateDomainBlock(ctx, domainBlock, columns...); err != nil {
|
||||
err = gtserror.Newf("db error updating domain block: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.apiDomainPerm(ctx, domainBlock, false)
|
||||
}
|
||||
|
||||
func (p *Processor) deleteDomainBlock(
|
||||
ctx context.Context,
|
||||
adminAcct *gtsmodel.Account,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -29,6 +30,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// DomainPermissionCreate creates an instance-level permission
|
||||
|
|
@ -84,6 +86,50 @@ func (p *Processor) DomainPermissionCreate(
|
|||
}
|
||||
}
|
||||
|
||||
// DomainPermissionUpdate updates a domain permission
|
||||
// of the given permissionType, with the given ID.
|
||||
func (p *Processor) DomainPermissionUpdate(
|
||||
ctx context.Context,
|
||||
permissionType gtsmodel.DomainPermissionType,
|
||||
permID string,
|
||||
obfuscate *bool,
|
||||
publicComment *string,
|
||||
privateComment *string,
|
||||
subscriptionID *string,
|
||||
) (*apimodel.DomainPermission, gtserror.WithCode) {
|
||||
switch permissionType {
|
||||
|
||||
// Explicitly block a domain.
|
||||
case gtsmodel.DomainPermissionBlock:
|
||||
return p.updateDomainBlock(
|
||||
ctx,
|
||||
permID,
|
||||
obfuscate,
|
||||
publicComment,
|
||||
privateComment,
|
||||
subscriptionID,
|
||||
)
|
||||
|
||||
// Explicitly allow a domain.
|
||||
case gtsmodel.DomainPermissionAllow:
|
||||
return p.updateDomainAllow(
|
||||
ctx,
|
||||
permID,
|
||||
obfuscate,
|
||||
publicComment,
|
||||
privateComment,
|
||||
subscriptionID,
|
||||
)
|
||||
|
||||
// 🎵 Why don't we all strap bombs to our chests,
|
||||
// and ride our bikes to the next G7 picnic?
|
||||
// Seems easier with every clock-tick. 🎵
|
||||
default:
|
||||
err := gtserror.Newf("unrecognized permission type %d", permissionType)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// DomainPermissionDelete removes one domain block with the given ID,
|
||||
// and processes side effects of removing the block asynchronously.
|
||||
//
|
||||
|
|
@ -153,14 +199,14 @@ func (p *Processor) DomainPermissionsImport(
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
// Parse file as slice of domain blocks.
|
||||
domainPerms := make([]*apimodel.DomainPermission, 0)
|
||||
if err := json.NewDecoder(file).Decode(&domainPerms); err != nil {
|
||||
// Parse file as slice of domain permissions.
|
||||
apiDomainPerms := make([]*apimodel.DomainPermission, 0)
|
||||
if err := json.NewDecoder(file).Decode(&apiDomainPerms); err != nil {
|
||||
err = gtserror.Newf("error parsing attachment as domain permissions: %w", err)
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
count := len(domainPerms)
|
||||
count := len(apiDomainPerms)
|
||||
if count == 0 {
|
||||
err = gtserror.New("error importing domain permissions: 0 entries provided")
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
|
|
@ -170,52 +216,97 @@ func (p *Processor) DomainPermissionsImport(
|
|||
// between successes and errors so that the caller can
|
||||
// try failed imports again if desired.
|
||||
multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count)
|
||||
|
||||
for _, domainPerm := range domainPerms {
|
||||
var (
|
||||
domain = domainPerm.Domain.Domain
|
||||
obfuscate = domainPerm.Obfuscate
|
||||
publicComment = domainPerm.PublicComment
|
||||
privateComment = domainPerm.PrivateComment
|
||||
subscriptionID = "" // No sub ID for imports.
|
||||
errWithCode gtserror.WithCode
|
||||
for _, apiDomainPerm := range apiDomainPerms {
|
||||
multiStatusEntries = append(
|
||||
multiStatusEntries,
|
||||
p.importOrUpdateDomainPerm(
|
||||
ctx,
|
||||
permissionType,
|
||||
account,
|
||||
apiDomainPerm,
|
||||
),
|
||||
)
|
||||
|
||||
domainPerm, _, errWithCode = p.DomainPermissionCreate(
|
||||
ctx,
|
||||
permissionType,
|
||||
account,
|
||||
domain,
|
||||
obfuscate,
|
||||
publicComment,
|
||||
privateComment,
|
||||
subscriptionID,
|
||||
)
|
||||
|
||||
var entry *apimodel.MultiStatusEntry
|
||||
|
||||
if errWithCode != nil {
|
||||
entry = &apimodel.MultiStatusEntry{
|
||||
// Use the failed domain entry as the resource value.
|
||||
Resource: domain,
|
||||
Message: errWithCode.Safe(),
|
||||
Status: errWithCode.Code(),
|
||||
}
|
||||
} else {
|
||||
entry = &apimodel.MultiStatusEntry{
|
||||
// Use successfully created API model domain block as the resource value.
|
||||
Resource: domainPerm,
|
||||
Message: http.StatusText(http.StatusOK),
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
multiStatusEntries = append(multiStatusEntries, *entry)
|
||||
}
|
||||
|
||||
return apimodel.NewMultiStatus(multiStatusEntries), nil
|
||||
}
|
||||
|
||||
func (p *Processor) importOrUpdateDomainPerm(
|
||||
ctx context.Context,
|
||||
permType gtsmodel.DomainPermissionType,
|
||||
account *gtsmodel.Account,
|
||||
apiDomainPerm *apimodel.DomainPermission,
|
||||
) apimodel.MultiStatusEntry {
|
||||
var (
|
||||
domain = apiDomainPerm.Domain.Domain
|
||||
obfuscate = apiDomainPerm.Obfuscate
|
||||
publicComment = cmp.Or(apiDomainPerm.PublicComment, apiDomainPerm.Comment)
|
||||
privateComment = apiDomainPerm.PrivateComment
|
||||
subscriptionID = "" // No sub ID for imports.
|
||||
)
|
||||
|
||||
// Check if this domain
|
||||
// perm already exists.
|
||||
var (
|
||||
domainPerm gtsmodel.DomainPermission
|
||||
err error
|
||||
)
|
||||
if permType == gtsmodel.DomainPermissionBlock {
|
||||
domainPerm, err = p.state.DB.GetDomainBlock(ctx, domain)
|
||||
} else {
|
||||
domainPerm, err = p.state.DB.GetDomainAllow(ctx, domain)
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real db error.
|
||||
return apimodel.MultiStatusEntry{
|
||||
Resource: domain,
|
||||
Message: "db error checking for existence of domain permission",
|
||||
Status: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
var errWithCode gtserror.WithCode
|
||||
if domainPerm != nil {
|
||||
// Permission already exists, update it.
|
||||
apiDomainPerm, errWithCode = p.DomainPermissionUpdate(
|
||||
ctx,
|
||||
permType,
|
||||
domainPerm.GetID(),
|
||||
obfuscate,
|
||||
publicComment,
|
||||
privateComment,
|
||||
nil,
|
||||
)
|
||||
} else {
|
||||
// Permission didn't exist yet, create it.
|
||||
apiDomainPerm, _, errWithCode = p.DomainPermissionCreate(
|
||||
ctx,
|
||||
permType,
|
||||
account,
|
||||
domain,
|
||||
util.PtrOrZero(obfuscate),
|
||||
util.PtrOrZero(publicComment),
|
||||
util.PtrOrZero(privateComment),
|
||||
subscriptionID,
|
||||
)
|
||||
}
|
||||
|
||||
if errWithCode != nil {
|
||||
return apimodel.MultiStatusEntry{
|
||||
Resource: domain,
|
||||
Message: errWithCode.Safe(),
|
||||
Status: errWithCode.Code(),
|
||||
}
|
||||
}
|
||||
|
||||
return apimodel.MultiStatusEntry{
|
||||
Resource: apiDomainPerm,
|
||||
Message: http.StatusText(http.StatusOK),
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
// DomainPermissionsGet returns all existing domain
|
||||
// permissions of the requested type. If export is
|
||||
// true, the format will be suitable for writing out
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool,
|
|||
}
|
||||
|
||||
domains = append(domains, &apimodel.Domain{
|
||||
Domain: d,
|
||||
SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt),
|
||||
PublicComment: domainBlock.PublicComment,
|
||||
Domain: d,
|
||||
SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt),
|
||||
Comment: &domainBlock.PublicComment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ func (p *Processor) byURI(
|
|||
|
||||
if includeAccounts(queryType) {
|
||||
// Check if URI points to an account.
|
||||
foundAccount, err := p.accountByURI(ctx, requestingAccount, uri, resolve)
|
||||
foundAccounts, err := p.accountsByURI(ctx, requestingAccount, uri, resolve)
|
||||
if err != nil {
|
||||
// Check for semi-expected error types.
|
||||
// On one of these, we can continue.
|
||||
|
|
@ -508,7 +508,9 @@ func (p *Processor) byURI(
|
|||
} else {
|
||||
// Hit! Return early since it's extremely unlikely
|
||||
// a status and an account will have the same URL.
|
||||
appendAccount(foundAccount)
|
||||
for _, foundAccount := range foundAccounts {
|
||||
appendAccount(foundAccount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -544,35 +546,42 @@ func (p *Processor) byURI(
|
|||
return nil
|
||||
}
|
||||
|
||||
// accountByURI looks for one account with the given URI.
|
||||
// accountsByURI looks for one account with the given URI/ID,
|
||||
// then if nothing is found, multiple accounts with the given URL.
|
||||
//
|
||||
// If resolve is false, it will only look in the database.
|
||||
// If resolve is true, it will try to resolve the account
|
||||
// from remote using the URI, if necessary.
|
||||
//
|
||||
// Will return either a hit, ErrNotRetrievable, ErrWrongType,
|
||||
// or a real error that the caller should handle.
|
||||
func (p *Processor) accountByURI(
|
||||
func (p *Processor) accountsByURI(
|
||||
ctx context.Context,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
uri *url.URL,
|
||||
resolve bool,
|
||||
) (*gtsmodel.Account, error) {
|
||||
) ([]*gtsmodel.Account, error) {
|
||||
if resolve {
|
||||
// We're allowed to resolve, leave the
|
||||
// rest up to the dereferencer functions.
|
||||
//
|
||||
// Allow dereferencing by URL and not just URI;
|
||||
// there are many cases where someone might
|
||||
// paste a URL into the search bar.
|
||||
account, _, err := p.federator.GetAccountByURI(
|
||||
gtscontext.SetFastFail(ctx),
|
||||
requestingAccount.Username,
|
||||
uri,
|
||||
true,
|
||||
)
|
||||
|
||||
return account, err
|
||||
return []*gtsmodel.Account{account}, err
|
||||
}
|
||||
|
||||
// We're not allowed to resolve; search database only.
|
||||
uriStr := uri.String() // stringify uri just once
|
||||
|
||||
// Search by ActivityPub URI.
|
||||
// Search for single acct by ActivityPub URI.
|
||||
account, err := p.state.DB.GetAccountByURI(ctx, uriStr)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err = gtserror.Newf("error checking database for account using URI %s: %w", uriStr, err)
|
||||
|
|
@ -581,22 +590,22 @@ func (p *Processor) accountByURI(
|
|||
|
||||
if account != nil {
|
||||
// We got a hit! No need to continue.
|
||||
return account, nil
|
||||
return []*gtsmodel.Account{account}, nil
|
||||
}
|
||||
|
||||
// No hit yet. Fallback to try by URL.
|
||||
account, err = p.state.DB.GetAccountByURL(ctx, uriStr)
|
||||
// No hit yet. Fallback to look for any accounts with URL.
|
||||
accounts, err := p.state.DB.GetAccountsByURL(ctx, uriStr)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err = gtserror.Newf("error checking database for account using URL %s: %w", uriStr, err)
|
||||
err = gtserror.Newf("error checking database for accounts using URL %s: %w", uriStr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if account != nil {
|
||||
// We got a hit! No need to continue.
|
||||
return account, nil
|
||||
if len(accounts) != 0 {
|
||||
// We got hits! No need to continue.
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf("account %s could not be retrieved locally and we cannot resolve", uriStr)
|
||||
err = fmt.Errorf("account(s) %s could not be retrieved locally and we cannot resolve", uriStr)
|
||||
return nil, gtserror.SetUnretrievable(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -303,10 +303,13 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) e
|
|||
}
|
||||
|
||||
// Account to which the Move is taking place.
|
||||
//
|
||||
// Match by uri only.
|
||||
targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
|
||||
ctx,
|
||||
fMsg.Receiving.Username,
|
||||
targetAcctURI,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ func (s *Subscriptions) processDomainPermission(
|
|||
Obfuscate: wantedPerm.GetObfuscate(),
|
||||
SubscriptionID: permSub.ID,
|
||||
}
|
||||
insertF = func() error { return s.state.DB.CreateDomainBlock(ctx, domainBlock) }
|
||||
insertF = func() error { return s.state.DB.PutDomainBlock(ctx, domainBlock) }
|
||||
|
||||
action = >smodel.AdminAction{
|
||||
ID: id.NewULID(),
|
||||
|
|
@ -461,7 +461,7 @@ func (s *Subscriptions) processDomainPermission(
|
|||
Obfuscate: wantedPerm.GetObfuscate(),
|
||||
SubscriptionID: permSub.ID,
|
||||
}
|
||||
insertF = func() error { return s.state.DB.CreateDomainAllow(ctx, domainAllow) }
|
||||
insertF = func() error { return s.state.DB.PutDomainAllow(ctx, domainAllow) }
|
||||
|
||||
action = >smodel.AdminAction{
|
||||
ID: id.NewULID(),
|
||||
|
|
@ -564,13 +564,13 @@ func permsFromCSV(
|
|||
|
||||
for i, columnHeader := range columnHeaders {
|
||||
// Remove leading # if present.
|
||||
normal := strings.TrimLeft(columnHeader, "#")
|
||||
columnHeader = strings.TrimLeft(columnHeader, "#")
|
||||
|
||||
// Find index of each column header we
|
||||
// care about, ensuring no duplicates.
|
||||
switch normal {
|
||||
switch {
|
||||
|
||||
case "domain":
|
||||
case columnHeader == "domain":
|
||||
if domainI != nil {
|
||||
body.Close()
|
||||
err := gtserror.NewfAt(3, "duplicate domain column header in csv: %+v", columnHeaders)
|
||||
|
|
@ -578,7 +578,7 @@ func permsFromCSV(
|
|||
}
|
||||
domainI = &i
|
||||
|
||||
case "severity":
|
||||
case columnHeader == "severity":
|
||||
if severityI != nil {
|
||||
body.Close()
|
||||
err := gtserror.NewfAt(3, "duplicate severity column header in csv: %+v", columnHeaders)
|
||||
|
|
@ -586,15 +586,15 @@ func permsFromCSV(
|
|||
}
|
||||
severityI = &i
|
||||
|
||||
case "public_comment":
|
||||
case columnHeader == "public_comment" || columnHeader == "comment":
|
||||
if publicCommentI != nil {
|
||||
body.Close()
|
||||
err := gtserror.NewfAt(3, "duplicate public_comment column header in csv: %+v", columnHeaders)
|
||||
err := gtserror.NewfAt(3, "duplicate public_comment or comment column header in csv: %+v", columnHeaders)
|
||||
return nil, err
|
||||
}
|
||||
publicCommentI = &i
|
||||
|
||||
case "obfuscate":
|
||||
case columnHeader == "obfuscate":
|
||||
if obfuscateI != nil {
|
||||
body.Close()
|
||||
err := gtserror.NewfAt(3, "duplicate obfuscate column header in csv: %+v", columnHeaders)
|
||||
|
|
@ -674,15 +674,15 @@ func permsFromCSV(
|
|||
perm.SetPublicComment(record[*publicCommentI])
|
||||
}
|
||||
|
||||
var obfuscate bool
|
||||
if obfuscateI != nil {
|
||||
obfuscate, err := strconv.ParseBool(record[*obfuscateI])
|
||||
obfuscate, err = strconv.ParseBool(record[*obfuscateI])
|
||||
if err != nil {
|
||||
l.Warnf("couldn't parse obfuscate field of record: %+v", record)
|
||||
continue
|
||||
}
|
||||
|
||||
perm.SetObfuscate(&obfuscate)
|
||||
}
|
||||
perm.SetObfuscate(&obfuscate)
|
||||
|
||||
// We're done.
|
||||
perms = append(perms, perm)
|
||||
|
|
@ -742,8 +742,9 @@ func permsFromJSON(
|
|||
}
|
||||
|
||||
// Set remaining fields.
|
||||
perm.SetPublicComment(apiPerm.PublicComment)
|
||||
perm.SetObfuscate(&apiPerm.Obfuscate)
|
||||
publicComment := cmp.Or(apiPerm.PublicComment, apiPerm.Comment)
|
||||
perm.SetPublicComment(util.PtrOrZero(publicComment))
|
||||
perm.SetObfuscate(util.Ptr(util.PtrOrZero(apiPerm.Obfuscate)))
|
||||
|
||||
// We're done.
|
||||
perms = append(perms, perm)
|
||||
|
|
@ -792,9 +793,15 @@ func permsFromPlain(
|
|||
var perm gtsmodel.DomainPermission
|
||||
switch permType {
|
||||
case gtsmodel.DomainPermissionBlock:
|
||||
perm = >smodel.DomainBlock{Domain: domain}
|
||||
perm = >smodel.DomainBlock{
|
||||
Domain: domain,
|
||||
Obfuscate: util.Ptr(false),
|
||||
}
|
||||
case gtsmodel.DomainPermissionAllow:
|
||||
perm = >smodel.DomainAllow{Domain: domain}
|
||||
perm = >smodel.DomainAllow{
|
||||
Domain: domain,
|
||||
Obfuscate: util.Ptr(false),
|
||||
}
|
||||
}
|
||||
|
||||
// We're done.
|
||||
|
|
|
|||
|
|
@ -775,7 +775,7 @@ func (suite *SubscriptionsTestSuite) TestAdoption() {
|
|||
existingBlock2,
|
||||
existingBlock3,
|
||||
} {
|
||||
if err := testStructs.State.DB.CreateDomainBlock(
|
||||
if err := testStructs.State.DB.PutDomainBlock(
|
||||
ctx, block,
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
|
|
@ -876,7 +876,7 @@ func (suite *SubscriptionsTestSuite) TestDomainAllowsAndBlocks() {
|
|||
}
|
||||
|
||||
// Store existing allow.
|
||||
if err := testStructs.State.DB.CreateDomainAllow(ctx, existingAllow); err != nil {
|
||||
if err := testStructs.State.DB.PutDomainAllow(ctx, existingAllow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
|
|||
suite.Equal(testAccountBefore.DisplayName, testAccountAfter.DisplayName)
|
||||
suite.Equal(testAccountBefore.Note, testAccountAfter.Note)
|
||||
suite.Equal(testAccountBefore.NoteRaw, testAccountAfter.NoteRaw)
|
||||
suite.Equal(testAccountBefore.Memorial, testAccountAfter.Memorial)
|
||||
suite.Equal(testAccountBefore.Bot, testAccountAfter.Bot)
|
||||
suite.Equal(testAccountBefore.MemorializedAt, testAccountAfter.MemorializedAt)
|
||||
suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked)
|
||||
suite.Equal(testAccountBefore.URI, testAccountAfter.URI)
|
||||
suite.Equal(testAccountBefore.URL, testAccountAfter.URL)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ type Account struct {
|
|||
DisplayName string `json:"displayName,omitempty" bun:",nullzero"`
|
||||
Note string `json:"note,omitempty" bun:",nullzero"`
|
||||
NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"`
|
||||
Memorial *bool `json:"memorial"`
|
||||
Bot *bool `json:"bot"`
|
||||
Locked *bool `json:"locked"`
|
||||
Discoverable *bool `json:"discoverable"`
|
||||
URI string `json:"uri" bun:",nullzero"`
|
||||
|
|
@ -45,7 +43,7 @@ type Account struct {
|
|||
FollowingURI string `json:"followingUri" bun:",nullzero"`
|
||||
FollowersURI string `json:"followersUri" bun:",nullzero"`
|
||||
FeaturedCollectionURI string `json:"featuredCollectionUri" bun:",nullzero"`
|
||||
ActorType string `json:"actorType" bun:",nullzero"`
|
||||
ActorType int16 `json:"actorType" bun:",nullzero"`
|
||||
PrivateKey *rsa.PrivateKey `json:"-" mapstructure:"-"`
|
||||
PrivateKeyString string `json:"privateKey,omitempty" mapstructure:"privateKey" bun:"-"`
|
||||
PublicKey *rsa.PublicKey `json:"-" mapstructure:"-"`
|
||||
|
|
|
|||
|
|
@ -70,19 +70,10 @@ func (c *Converter) ASRepresentationToAccount(
|
|||
acct.URI = uri
|
||||
|
||||
// Check whether account is a usable actor type.
|
||||
switch acct.ActorType = accountable.GetTypeName(); acct.ActorType {
|
||||
|
||||
// people, groups, and organizations aren't bots
|
||||
case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization:
|
||||
acct.Bot = util.Ptr(false)
|
||||
|
||||
// apps and services are
|
||||
case ap.ActorApplication, ap.ActorService:
|
||||
acct.Bot = util.Ptr(true)
|
||||
|
||||
// we don't know what this is!
|
||||
default:
|
||||
err := gtserror.Newf("unusable actor type for %s", uri)
|
||||
actorTypeName := accountable.GetTypeName()
|
||||
acct.ActorType = gtsmodel.ParseAccountActorType(actorTypeName)
|
||||
if acct.ActorType == gtsmodel.AccountActorTypeUnknown {
|
||||
err := gtserror.Newf("unusable actor type %s for %s", actorTypeName, uri)
|
||||
return nil, gtserror.SetMalformed(err)
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +152,7 @@ func (c *Converter) ASRepresentationToAccount(
|
|||
acct.Note = ap.ExtractSummary(accountable)
|
||||
|
||||
// Assume not memorial (todo)
|
||||
acct.Memorial = util.Ptr(false)
|
||||
acct.MemorializedAt = time.Time{}
|
||||
|
||||
// Extract 'manuallyApprovesFollowers' aka locked account (default = true).
|
||||
manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable)
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
|
|||
suite.Equal("https://owncast.example.org/logo/external", acct.HeaderRemoteURL)
|
||||
suite.Equal("Rob's Owncast Server", acct.DisplayName)
|
||||
suite.Equal("linux audio stuff", acct.Note)
|
||||
suite.True(*acct.Bot)
|
||||
suite.False(*acct.Locked)
|
||||
suite.True(*acct.Discoverable)
|
||||
suite.Equal("https://owncast.example.org/federation/user/rgh", acct.URI)
|
||||
|
|
@ -212,7 +211,7 @@ func (suite *ASToInternalTestSuite) TestParseOwncastService() {
|
|||
suite.Equal("https://owncast.example.org/federation/user/rgh/inbox", acct.InboxURI)
|
||||
suite.Equal("https://owncast.example.org/federation/user/rgh/outbox", acct.OutboxURI)
|
||||
suite.Equal("https://owncast.example.org/federation/user/rgh/followers", acct.FollowersURI)
|
||||
suite.Equal("Service", acct.ActorType)
|
||||
suite.Equal(gtsmodel.AccountActorTypeService, acct.ActorType)
|
||||
suite.Equal("https://owncast.example.org/federation/user/rgh#main-key", acct.PublicKeyURI)
|
||||
|
||||
acct.ID = "01G42D57DTCJQE8XT9KD4K88RK"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
)
|
||||
|
||||
|
|
@ -49,7 +48,7 @@ func (c *Converter) AccountToAS(
|
|||
// accountable is a service if this
|
||||
// is a bot account, otherwise a person.
|
||||
var accountable ap.Accountable
|
||||
if util.PtrOrZero(a.Bot) {
|
||||
if a.ActorType.IsBot() {
|
||||
accountable = streams.NewActivityStreamsService()
|
||||
} else {
|
||||
accountable = streams.NewActivityStreamsPerson()
|
||||
|
|
@ -393,7 +392,7 @@ func (c *Converter) AccountToASMinimal(
|
|||
// accountable is a service if this
|
||||
// is a bot account, otherwise a person.
|
||||
var accountable ap.Accountable
|
||||
if util.PtrOrZero(a.Bot) {
|
||||
if a.ActorType.IsBot() {
|
||||
accountable = streams.NewActivityStreamsService()
|
||||
} else {
|
||||
accountable = streams.NewActivityStreamsPerson()
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
|
|
@ -100,7 +99,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
|
|||
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
|
||||
|
||||
// Update zork to be a bot.
|
||||
testAccount.Bot = util.Ptr(true)
|
||||
testAccount.ActorType = gtsmodel.AccountActorTypeService
|
||||
if err := suite.state.DB.UpdateAccount(context.Background(), testAccount); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -365,7 +365,6 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
var (
|
||||
locked = util.PtrOrValue(a.Locked, true)
|
||||
discoverable = util.PtrOrValue(a.Discoverable, false)
|
||||
bot = util.PtrOrValue(a.Bot, false)
|
||||
)
|
||||
|
||||
// Remaining properties are simple and
|
||||
|
|
@ -378,7 +377,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
DisplayName: a.DisplayName,
|
||||
Locked: locked,
|
||||
Discoverable: discoverable,
|
||||
Bot: bot,
|
||||
Bot: a.ActorType.IsBot(),
|
||||
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
||||
Note: a.Note,
|
||||
URL: a.URL,
|
||||
|
|
@ -522,7 +521,7 @@ func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
|
|||
ID: a.ID,
|
||||
Username: a.Username,
|
||||
Acct: acct,
|
||||
Bot: *a.Bot,
|
||||
Bot: a.ActorType.IsBot(),
|
||||
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
||||
URL: a.URL,
|
||||
// Empty array (not nillable).
|
||||
|
|
@ -2186,7 +2185,7 @@ func (c *Converter) DomainPermToAPIDomainPerm(
|
|||
domainPerm := &apimodel.DomainPermission{
|
||||
Domain: apimodel.Domain{
|
||||
Domain: domain,
|
||||
PublicComment: d.GetPublicComment(),
|
||||
PublicComment: util.Ptr(d.GetPublicComment()),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -2197,8 +2196,8 @@ func (c *Converter) DomainPermToAPIDomainPerm(
|
|||
}
|
||||
|
||||
domainPerm.ID = d.GetID()
|
||||
domainPerm.Obfuscate = util.PtrOrZero(d.GetObfuscate())
|
||||
domainPerm.PrivateComment = d.GetPrivateComment()
|
||||
domainPerm.Obfuscate = d.GetObfuscate()
|
||||
domainPerm.PrivateComment = util.Ptr(d.GetPrivateComment())
|
||||
domainPerm.SubscriptionID = d.GetSubscriptionID()
|
||||
domainPerm.CreatedBy = d.GetCreatedByAccountID()
|
||||
if createdAt := d.GetCreatedAt(); !createdAt.IsZero() {
|
||||
|
|
|
|||
|
|
@ -404,7 +404,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendPubl
|
|||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"bot": true,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@localhost:8080",
|
||||
|
|
@ -444,7 +444,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
|
|||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"bot": true,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@localhost:8080",
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ package util
|
|||
|
||||
import "time"
|
||||
|
||||
// ISO8601 is a formatter for serializing times that forces ISO8601 behavior.
|
||||
const ISO8601 = "2006-01-02T15:04:05.000Z"
|
||||
const ISO8601Date = "2006-01-02"
|
||||
const (
|
||||
ISO8601 = "2006-01-02T15:04:05.000Z"
|
||||
ISO8601Date = "2006-01-02"
|
||||
RFC2822 = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
)
|
||||
|
||||
// FormatISO8601 converts the given time to UTC and then formats it
|
||||
// using the ISO8601 const, which the Mastodon API is able to understand.
|
||||
|
|
@ -39,3 +41,11 @@ func FormatISO8601Date(t time.Time) string {
|
|||
func ParseISO8601(in string) (time.Time, error) {
|
||||
return time.Parse(ISO8601, in)
|
||||
}
|
||||
|
||||
// FormatRFC2822 converts the given time to local and then formats it using
|
||||
// the RFC2822 const, which conforms with email Date header requirements.
|
||||
//
|
||||
// See: https://www.rfc-editor.org/rfc/rfc2822#section-3.3
|
||||
func FormatRFC2822(t time.Time) string {
|
||||
return t.Local().Format(RFC2822)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue