[chore] Deinterface processor and subprocessors (#1501)

* [chore] Deinterface processor and subprocessors

* expose subprocessors via function calls

* missing license header
This commit is contained in:
tobi
2023-02-22 16:05:26 +01:00
committed by GitHub
parent adb596600b
commit b6fbdc66c1
246 changed files with 2545 additions and 4549 deletions

View File

@@ -1,92 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form)
}
func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode {
return p.accountProcessor.DeleteLocal(ctx, authed.Account, form)
}
func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.Get(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username)
}
func (p *processor) AccountGetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) {
return p.accountProcessor.GetCustomCSSForUsername(ctx, username)
}
func (p *processor) AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) {
return p.accountProcessor.GetRSSFeedForUsername(ctx, username)
}
func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.Update(ctx, authed.Account, form)
}
func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
}
func (p *processor) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.accountProcessor.WebStatusesGet(ctx, targetAccountID, maxID)
}
func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.FollowersGet(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountFollowingGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.FollowingGet(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountRelationshipGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.RelationshipGet(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountFollowCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.FollowCreate(ctx, authed.Account, form)
}
func (p *processor) AccountFollowRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.FollowRemove(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountBlockCreate(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.BlockCreate(ctx, authed.Account, targetAccountID)
}
func (p *processor) AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.BlockRemove(ctx, authed.Account, targetAccountID)
}

View File

@@ -19,15 +19,9 @@
package account
import (
"context"
"mime/multipart"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
@@ -35,63 +29,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/oauth2/v4"
)
// Processor wraps a bunch of functions for processing account actions.
type Processor interface {
// Create processes the given form for creating a new account, returning an oauth token for that account if successful.
Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode)
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block.
Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode
// DeleteLocal is like delete, but specifically for deletion of local accounts rather than federated ones.
// Unlike Delete, it will propagate the deletion out across the federating API to other instances.
DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode
// Get processes the given request for account information.
Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
// GetLocalByUsername processes the given request for account information targeting a local account by username.
GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode)
// GetCustomCSSForUsername returns custom css for the given local username.
GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode)
// GetRSSFeedForUsername returns RSS feed for the given local username.
GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode)
// Update processes the update of an account with the given form
Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode)
// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
// statuses which are suitable for showing on the public web profile of an account.
WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers.
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// FollowingGet fetches a list of the accounts that target account is following.
FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// FollowCreate handles a follow request to an account, either remote or local.
FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// UpdateAvatar does the dirty work of checking the avatar part of an account update form,
// parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new avatar image.
UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error)
// UpdateHeader does the dirty work of checking the header part of an account update form,
// parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new header image.
UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error)
}
type processor struct {
// Processor wraps functionality for updating, creating, and deleting accounts in response to API requests.
//
// It also contains logic for actions towards accounts such as following, blocking, seeing follows, etc.
type Processor struct {
tc typeutils.TypeConverter
mediaManager media.Manager
clientWorker *concurrency.WorkerPool[messages.FromClientAPI]
@@ -104,8 +47,16 @@ type processor struct {
}
// New returns a new account processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, oauthServer oauth.Server, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], federator federation.Federator, parseMention gtsmodel.ParseMentionFunc) Processor {
return &processor{
func New(
db db.DB,
tc typeutils.TypeConverter,
mediaManager media.Manager,
oauthServer oauth.Server,
clientWorker *concurrency.WorkerPool[messages.FromClientAPI],
federator federation.Federator,
parseMention gtsmodel.ParseMentionFunc,
) Processor {
return Processor{
tc: tc,
mediaManager: mediaManager,
clientWorker: clientWorker,

View File

@@ -20,6 +20,7 @@ package account
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
@@ -32,7 +33,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// make sure the target account actually exists in our db
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
@@ -154,3 +156,37 @@ func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// make sure the target account actually exists in our db
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))
}
// check if a block exists, and remove it if it does
block, err := p.db.GetBlock(ctx, requestingAccount.ID, targetAccountID)
if err == nil {
// we got a block, remove it
block.Account = requestingAccount
block.TargetAccount = targetAccount
if err := p.db.DeleteBlockByID(ctx, block.ID); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err))
}
// send the UNDO activity to the client worker for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityUndo,
GTSModel: block,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
} else if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err))
}
// return whatever relationship results from all this
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}

View File

@@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) {
func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) {
if requestingAccount == nil {
return nil, gtserror.NewErrorForbidden(fmt.Errorf("cannot retrieve bookmarks without a requesting account"))
}

View File

@@ -33,7 +33,8 @@ import (
"github.com/superseriousbusiness/oauth2/v4"
)
func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
// Create processes the given form for creating a new account, returning an oauth token for that account if successful.
func (p *Processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
emailAvailable, err := p.db.IsEmailAvailable(ctx, form.Email)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err)

View File

@@ -34,28 +34,9 @@ import (
"golang.org/x/crypto/bcrypt"
)
// Delete handles the complete deletion of an account.
//
// To be done in this function:
// 1. Delete account's application(s), clients, and oauth tokens
// 2. Delete account's blocks
// 3. Delete account's emoji
// 4. Delete account's follow requests
// 5. Delete account's follows
// 6. Delete account's statuses
// 7. Delete account's media attachments
// 8. Delete account's mentions
// 9. Delete account's polls
// 10. Delete account's notifications
// 11. Delete account's bookmarks
// 12. Delete account's faves
// 13. Delete account's mutes
// 14. Delete account's streams
// 15. Delete account's tags
// 16. Delete account's user
// 17. Delete account's timeline
// 18. Delete account itself
func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode {
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block.
func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode {
fields := kv.Fields{{"username", account.Username}}
if account.Domain != "" {
@@ -289,7 +270,9 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
return nil
}
func (p *processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode {
// DeleteLocal is like Delete, but specifically for deletion of local accounts rather than federated ones.
// Unlike Delete, it will propagate the deletion out across the federating API to other instances.
func (p *Processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode {
fromClientAPIMessage := messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,

View File

@@ -32,7 +32,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
// FollowCreate handles a follow request to an account, either remote or local.
func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't create the request ofc
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
@@ -119,3 +120,86 @@ func (p *processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode
// return whatever relationship results from this
return p.RelationshipGet(ctx, requestingAccount, form.ID)
}
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't do anything
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
}
// make sure the target account actually exists in our db
targetAcct, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
}
}
// check if a follow request exists, and remove it if it does (storing the URI for later)
var frChanged bool
var frURI string
fr := &gtsmodel.FollowRequest{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
}
frChanged = true
}
// now do the same thing for any existing follow
var fChanged bool
var fURI string
f := &gtsmodel.Follow{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.db.DeleteByID(ctx, f.ID, f); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
}
fChanged = true
}
// follow request status changed so send the UNDO activity to the channel for async processing
if frChanged {
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: frURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
})
}
// follow status changed so send the UNDO activity to the channel for async processing
if fChanged {
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
})
}
// return whatever relationship results from all this
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}

View File

@@ -31,7 +31,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
// Get processes the given request for account information.
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
if err == db.ErrNoEntries {
@@ -40,10 +41,11 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
}
return p.getAccountFor(ctx, requestingAccount, targetAccount)
return p.getFor(ctx, requestingAccount, targetAccount)
}
func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) {
// GetLocalByUsername processes the given request for account information targeting a local account by username.
func (p *Processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) {
targetAccount, err := p.db.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
if err == db.ErrNoEntries {
@@ -52,10 +54,11 @@ func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *g
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
}
return p.getAccountFor(ctx, requestingAccount, targetAccount)
return p.getFor(ctx, requestingAccount, targetAccount)
}
func (p *processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) {
// GetCustomCSSForUsername returns custom css for the given local username.
func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) {
customCSS, err := p.db.GetAccountCustomCSSByUsername(ctx, username)
if err != nil {
if err == db.ErrNoEntries {
@@ -67,7 +70,7 @@ func (p *processor) GetCustomCSSForUsername(ctx context.Context, username string
return customCSS, nil
}
func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) {
func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) {
var blocked bool
var err error
if requestingAccount != nil {

View File

@@ -1,74 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 account
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
accounts := []apimodel.Account{}
follows, err := p.db.GetAccountFollowedBy(ctx, targetAccountID, false)
if err != nil {
if err == db.ErrNoEntries {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range follows {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
if f.Account == nil {
a, err := p.db.GetAccountByID(ctx, f.AccountID)
if err != nil {
if err == db.ErrNoEntries {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
f.Account = a
}
account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
}

View File

@@ -1,47 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 account
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
if requestingAccount == nil {
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
}
gtsR, err := p.db.GetRelationship(ctx, requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
}
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
}
return r, nil
}

View File

@@ -20,6 +20,7 @@ package account
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@@ -28,7 +29,54 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
// FollowersGet fetches a list of the target account's followers.
func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
accounts := []apimodel.Account{}
follows, err := p.db.GetAccountFollowedBy(ctx, targetAccountID, false)
if err != nil {
if err == db.ErrNoEntries {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range follows {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
if f.Account == nil {
a, err := p.db.GetAccountByID(ctx, f.AccountID)
if err != nil {
if err == db.ErrNoEntries {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
f.Account = a
}
account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
}
// FollowingGet fetches a list of the accounts that target account is following.
func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
@@ -72,3 +120,22 @@ func (p *processor) FollowingGet(ctx context.Context, requestingAccount *gtsmode
}
return accounts, nil
}
// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
func (p *Processor) RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
if requestingAccount == nil {
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
}
gtsR, err := p.db.GetRelationship(ctx, requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
}
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
}
return r, nil
}

View File

@@ -1,65 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 account
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// make sure the target account actually exists in our db
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))
}
// check if a block exists, and remove it if it does
block, err := p.db.GetBlock(ctx, requestingAccount.ID, targetAccountID)
if err == nil {
// we got a block, remove it
block.Account = requestingAccount
block.TargetAccount = targetAccount
if err := p.db.DeleteBlockByID(ctx, block.ID); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err))
}
// send the UNDO activity to the client worker for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityUndo,
GTSModel: block,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
} else if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err))
}
// return whatever relationship results from all this
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}

View File

@@ -1,113 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 account
import (
"context"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't do anything
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
}
// make sure the target account actually exists in our db
targetAcct, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
}
}
// check if a follow request exists, and remove it if it does (storing the URI for later)
var frChanged bool
var frURI string
fr := &gtsmodel.FollowRequest{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
}
frChanged = true
}
// now do the same thing for any existing follow
var fChanged bool
var fURI string
f := &gtsmodel.Follow{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.db.DeleteByID(ctx, f.ID, f); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
}
fChanged = true
}
// follow request status changed so send the UNDO activity to the channel for async processing
if frChanged {
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: frURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
})
}
// follow status changed so send the UNDO activity to the channel for async processing
if fChanged {
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
})
}
// return whatever relationship results from all this
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}

View File

@@ -32,7 +32,8 @@ import (
const rssFeedLength = 20
func (p *processor) GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) {
// GetRSSFeedForUsername returns RSS feed for the given local username.
func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) {
account, err := p.db.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -29,7 +29,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) {
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
func (p *Processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) {
if requestingAccount != nil {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
@@ -96,7 +98,9 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel
})
}
func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) {
// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
// statuses which are suitable for showing on the public web profile of an account.
func (p *Processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) {
acct, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -33,10 +33,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
// Update processes the update of an account with the given form.
func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
if form.Discoverable != nil {
account.Discoverable = form.Discoverable
}
@@ -138,7 +140,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if err := validate.Privacy(*form.Source.Privacy); err != nil {
return nil, gtserror.NewErrorBadRequest(err)
}
privacy := p.tc.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
account.Privacy = privacy
}
@@ -185,7 +187,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
// UpdateAvatar does the dirty work of checking the avatar part of an account update form,
// parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new avatar image.
func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
func (p *Processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
maxImageSize := config.GetMediaImageMaxSize()
if avatar.Size > int64(maxImageSize) {
return nil, fmt.Errorf("UpdateAvatar: avatar with size %d exceeded max image size of %d bytes", avatar.Size, maxImageSize)
@@ -213,7 +215,7 @@ func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHead
// UpdateHeader does the dirty work of checking the header part of an account update form,
// parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new header image.
func (p *processor) UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
func (p *Processor) UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) {
maxImageSize := config.GetMediaImageMaxSize()
if header.Size > int64(maxImageSize) {
return nil, fmt.Errorf("UpdateHeader: header with size %d exceeded max image size of %d bytes", header.Size, maxImageSize)

View File

@@ -53,7 +53,7 @@ func (suite *AccountTestSuite) TestAccountDeleteLocal() {
err := suite.db.Put(ctx, follow)
suite.NoError(err)
errWithCode := suite.processor.AccountDeleteLocal(ctx, suite.testAutheds["local_account_1"], &apimodel.AccountDeleteRequest{
errWithCode := suite.processor.Account().DeleteLocal(ctx, suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{
Password: "password",
DeleteOriginID: deletingAccount.ID,
})

View File

@@ -1,95 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) AdminAccountAction(ctx context.Context, authed *oauth.Auth, form *apimodel.AdminAccountActionRequest) gtserror.WithCode {
return p.adminProcessor.AccountAction(ctx, authed.Account, form)
}
func (p *processor) AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
return p.adminProcessor.EmojiCreate(ctx, authed.Account, authed.User, form)
}
func (p *processor) AdminEmojisGet(ctx context.Context, authed *oauth.Auth, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.adminProcessor.EmojisGet(ctx, authed.Account, authed.User, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
}
func (p *processor) AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
return p.adminProcessor.EmojiGet(ctx, authed.Account, authed.User, id)
}
func (p *processor) AdminEmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) {
return p.adminProcessor.EmojiUpdate(ctx, id, form)
}
func (p *processor) AdminEmojiDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
return p.adminProcessor.EmojiDelete(ctx, id)
}
func (p *processor) AdminEmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) {
return p.adminProcessor.EmojiCategoriesGet(ctx)
}
func (p *processor) AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockCreate(ctx, authed.Account, form.Domain, form.Obfuscate, form.PublicComment, form.PrivateComment, "")
}
func (p *processor) AdminDomainBlocksImport(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) ([]*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlocksImport(ctx, authed.Account, form.Domains)
}
func (p *processor) AdminDomainBlocksGet(ctx context.Context, authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlocksGet(ctx, authed.Account, export)
}
func (p *processor) AdminDomainBlockGet(ctx context.Context, authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockGet(ctx, authed.Account, id, export)
}
func (p *processor) AdminDomainBlockDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockDelete(ctx, authed.Account, id)
}
func (p *processor) AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode {
return p.adminProcessor.MediaPrune(ctx, mediaRemoteCacheDays)
}
func (p *processor) AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode {
return p.adminProcessor.MediaRefetch(ctx, authed.Account, domain)
}
func (p *processor) AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.adminProcessor.ReportsGet(ctx, authed.Account, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit)
}
func (p *processor) AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode) {
return p.adminProcessor.ReportGet(ctx, authed.Account, id)
}
func (p *processor) AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) {
return p.adminProcessor.ReportResolve(ctx, authed.Account, id, actionTakenComment)
}

View File

@@ -1,3 +1,21 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
@@ -12,7 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode {
func (p *Processor) AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode {
targetAccount, err := p.db.GetAccountByID(ctx, form.TargetAccountID)
if err != nil {
return gtserror.NewErrorInternalError(err)

View File

@@ -19,14 +19,8 @@
package admin
import (
"context"
"mime/multipart"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/storage"
@@ -34,28 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// Processor wraps a bunch of functions for processing admin actions.
type Processor interface {
DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode)
DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode)
DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode
EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode)
EmojisGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode)
EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode)
MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode
ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode)
ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode)
}
type processor struct {
type Processor struct {
tc typeutils.TypeConverter
mediaManager media.Manager
transportController transport.Controller
@@ -66,7 +39,7 @@ type processor struct {
// New returns a new admin processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller, storage *storage.Driver, clientWorker *concurrency.WorkerPool[messages.FromClientAPI]) Processor {
return &processor{
return Processor{
tc: tc,
mediaManager: mediaManager,
transportController: transportController,

View File

@@ -1,89 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"fmt"
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
if maybeExisting != nil {
return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
}
if err != nil && err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err))
}
emojiID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID")
}
emojiURI := uris.GenerateURIForEmoji(emojiID)
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
f, err := form.Image.Open()
return f, form.Image.Size, err
}
var ai *media.AdditionalEmojiInfo
if form.CategoryName != "" {
category, err := p.GetOrCreateEmojiCategory(ctx, form.CategoryName)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting id in category: %s", err), "error putting id in category")
}
ai = &media.AdditionalEmojiInfo{
CategoryID: &category.ID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, ai, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
}
emoji, err := processingEmoji.LoadEmoji(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
}
apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation")
}
return &apiEmoji, nil
}

View File

@@ -1,86 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"fmt"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(ctx, id, domainBlock); err != nil {
if err != db.ErrNoEntries {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
// there are no entries for this ID
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
}
// prepare the domain block to return
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// Delete the domain block
if err := p.db.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// remove the domain block reference from the instance, if we have an entry for it
i := &gtsmodel.Instance{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "domain", Value: domainBlock.Domain},
{Key: "domain_block_id", Value: id},
}, i); err == nil {
updatingColumns := []string{"suspended_at", "domain_block_id", "updated_at"}
i.SuspendedAt = time.Time{}
i.DomainBlockID = ""
i.UpdatedAt = time.Now()
if err := p.db.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("couldn't update database entry for instance %s: %s", domainBlock.Domain, err))
}
}
// unsuspend all accounts whose suspension origin was this domain block
// 1. remove the 'suspended_at' entry from their accounts
if err := p.db.UpdateWhere(ctx, []db.Where{
{Key: "suspension_origin", Value: domainBlock.ID},
}, "suspended_at", nil, &[]*gtsmodel.Account{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspended_at from accounts: %s", err))
}
// 2. remove the 'suspension_origin' entry from their accounts
if err := p.db.UpdateWhere(ctx, []db.Where{
{Key: "suspension_origin", Value: domainBlock.ID},
}, "suspension_origin", nil, &[]*gtsmodel.Account{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspension_origin from accounts: %s", err))
}
return apiDomainBlock, nil
}

View File

@@ -1,59 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (p *processor) EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiDelete: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiDelete: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
if emoji.Domain != "" {
err = fmt.Errorf("EmojiDelete: emoji with id %s was not a local emoji, will not delete", id)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err = fmt.Errorf("EmojiDelete: error converting emoji to admin api emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
if err := p.db.DeleteEmojiByID(ctx, id); err != nil {
err := fmt.Errorf("EmojiDelete: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}

View File

@@ -1,27 +1,13 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"strings"
"time"
@@ -37,7 +23,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
)
func (p *processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) {
func (p *Processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) {
// domain blocks will always be lowercase
domain = strings.ToLower(domain)
@@ -88,12 +74,8 @@ func (p *processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Acc
// 1. Strip most info away from the instance entry for the domain.
// 2. Delete the instance account for that instance if it exists.
// 3. Select all accounts from this instance and pass them through the delete functionality of the processor.
func (p *processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) {
l := log.WithContext(ctx).
WithFields(kv.Fields{
{"domain", block.Domain},
}...)
func (p *Processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) {
l := log.WithContext(ctx).WithFields(kv.Fields{{"domain", block.Domain}}...)
l.Debug("processing domain block side effects")
// if we have an instance entry for this domain, update it with the new block ID and clear all fields
@@ -174,3 +156,139 @@ selectAccountsLoop:
}
}
}
// DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file.
func (p *Processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) {
f, err := domains.Open()
if err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err))
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error reading attachment: %s", err))
}
if size == 0 {
return nil, gtserror.NewErrorBadRequest(errors.New("DomainBlocksImport: could not read provided attachment: size 0 bytes"))
}
d := []apimodel.DomainBlock{}
if err := json.Unmarshal(buf.Bytes(), &d); err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: could not read provided attachment: %s", err))
}
blocks := []*apimodel.DomainBlock{}
for _, d := range d {
block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "")
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}
// DomainBlocksGet returns all existing domain blocks.
// If export is true, the format will be suitable for writing out to an export.
func (p *Processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
domainBlocks := []*gtsmodel.DomainBlock{}
if err := p.db.GetAll(ctx, &domainBlocks); err != nil {
if !errors.Is(err, db.ErrNoEntries) {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
}
apiDomainBlocks := []*apimodel.DomainBlock{}
for _, b := range domainBlocks {
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, b, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock)
}
return apiDomainBlocks, nil
}
// DomainBlockGet returns one domain block with the given id.
// If export is true, the format will be suitable for writing out to an export.
func (p *Processor) DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(ctx, id, domainBlock); err != nil {
if !errors.Is(err, db.ErrNoEntries) {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
// there are no entries for this ID
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
}
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apiDomainBlock, nil
}
// DomainBlockDelete removes one domain block with the given ID.
func (p *Processor) DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(ctx, id, domainBlock); err != nil {
if !errors.Is(err, db.ErrNoEntries) {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
// there are no entries for this ID
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
}
// prepare the domain block to return
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// Delete the domain block
if err := p.db.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// remove the domain block reference from the instance, if we have an entry for it
i := &gtsmodel.Instance{}
if err := p.db.GetWhere(ctx, []db.Where{
{Key: "domain", Value: domainBlock.Domain},
{Key: "domain_block_id", Value: id},
}, i); err == nil {
updatingColumns := []string{"suspended_at", "domain_block_id", "updated_at"}
i.SuspendedAt = time.Time{}
i.DomainBlockID = ""
i.UpdatedAt = time.Now()
if err := p.db.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("couldn't update database entry for instance %s: %s", domainBlock.Domain, err))
}
}
// unsuspend all accounts whose suspension origin was this domain block
// 1. remove the 'suspended_at' entry from their accounts
if err := p.db.UpdateWhere(ctx, []db.Where{
{Key: "suspension_origin", Value: domainBlock.ID},
}, "suspended_at", nil, &[]*gtsmodel.Account{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspended_at from accounts: %s", err))
}
// 2. remove the 'suspension_origin' entry from their accounts
if err := p.db.UpdateWhere(ctx, []db.Where{
{Key: "suspension_origin", Value: domainBlock.ID},
}, "suspension_origin", nil, &[]*gtsmodel.Account{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspension_origin from accounts: %s", err))
}
return apiDomainBlock, nil
}

View File

@@ -0,0 +1,485 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// EmojiCreate creates a custom emoji on this instance.
func (p *Processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
if maybeExisting != nil {
return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
}
if err != nil && err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err))
}
emojiID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID")
}
emojiURI := uris.GenerateURIForEmoji(emojiID)
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
f, err := form.Image.Open()
return f, form.Image.Size, err
}
var ai *media.AdditionalEmojiInfo
if form.CategoryName != "" {
category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting id in category: %s", err), "error putting id in category")
}
ai = &media.AdditionalEmojiInfo{
CategoryID: &category.ID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, ai, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
}
emoji, err := processingEmoji.LoadEmoji(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
}
apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation")
}
return &apiEmoji, nil
}
// EmojisGet returns an admin view of custom emojis, filtered with the given parameters.
func (p *Processor) EmojisGet(
ctx context.Context,
account *gtsmodel.Account,
user *gtsmodel.User,
domain string,
includeDisabled bool,
includeEnabled bool,
shortcode string,
maxShortcodeDomain string,
minShortcodeDomain string,
limit int,
) (*apimodel.PageableResponse, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
emojis, err := p.db.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := fmt.Errorf("EmojisGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(emojis)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
items := make([]interface{}, 0, count)
for _, emoji := range emojis {
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
items = append(items, adminEmoji)
}
filterBuilder := strings.Builder{}
filterBuilder.WriteString("filter=")
switch domain {
case "", "local":
filterBuilder.WriteString("domain:local")
case db.EmojiAllDomains:
filterBuilder.WriteString("domain:all")
default:
filterBuilder.WriteString("domain:")
filterBuilder.WriteString(domain)
}
if includeDisabled != includeEnabled {
if includeDisabled {
filterBuilder.WriteString(",disabled")
}
if includeEnabled {
filterBuilder.WriteString(",enabled")
}
}
if shortcode != "" {
filterBuilder.WriteString(",shortcode:")
filterBuilder.WriteString(shortcode)
}
return util.PackagePageableResponse(util.PageableResponseParams{
Items: items,
Path: "api/v1/admin/custom_emojis",
NextMaxIDKey: "max_shortcode_domain",
NextMaxIDValue: util.ShortcodeDomain(emojis[count-1]),
PrevMinIDKey: "min_shortcode_domain",
PrevMinIDValue: util.ShortcodeDomain(emojis[0]),
Limit: limit,
ExtraQueryParams: []string{filterBuilder.String()},
})
}
// EmojiGet returns the admin view of one custom emoji with the given id.
func (p *Processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// EmojiDelete deletes one emoji from the database, with the given id.
func (p *Processor) EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiDelete: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiDelete: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
if emoji.Domain != "" {
err = fmt.Errorf("EmojiDelete: emoji with id %s was not a local emoji, will not delete", id)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err = fmt.Errorf("EmojiDelete: error converting emoji to admin api emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
if err := p.db.DeleteEmojiByID(ctx, id); err != nil {
err := fmt.Errorf("EmojiDelete: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// EmojiUpdate updates one emoji with the given id, using the provided form parameters.
func (p *Processor) EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) {
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiUpdate: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiUpdate: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
switch form.Type {
case apimodel.EmojiUpdateCopy:
return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
case apimodel.EmojiUpdateDisable:
return p.emojiUpdateDisable(ctx, emoji)
case apimodel.EmojiUpdateModify:
return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
default:
err := errors.New("unrecognized emoji action type")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
}
// EmojiCategoriesGet returns all custom emoji categories that exist on this instance.
func (p *Processor) EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) {
categories, err := p.db.GetEmojiCategories(ctx)
if err != nil {
err := fmt.Errorf("EmojiCategoriesGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories))
for _, category := range categories {
apiCategory, err := p.tc.EmojiCategoryToAPIEmojiCategory(ctx, category)
if err != nil {
err := fmt.Errorf("EmojiCategoriesGet: error converting emoji category to api emoji category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiCategories = append(apiCategories, apiCategory)
}
return apiCategories, nil
}
/*
UTIL FUNCTIONS
*/
func (p *Processor) getOrCreateEmojiCategory(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) {
category, err := p.db.GetEmojiCategoryByName(ctx, name)
if err == nil {
return category, nil
}
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("GetOrCreateEmojiCategory: database error trying get emoji category by name: %s", err)
return nil, err
}
// we don't have the category yet, just create it with the given name
categoryID, err := id.NewRandomULID()
if err != nil {
err = fmt.Errorf("GetOrCreateEmojiCategory: error generating id for new emoji category: %s", err)
return nil, err
}
category = &gtsmodel.EmojiCategory{
ID: categoryID,
Name: name,
}
if err := p.db.PutEmojiCategory(ctx, category); err != nil {
err = fmt.Errorf("GetOrCreateEmojiCategory: error putting new emoji category in the database: %s", err)
return nil, err
}
return category, nil
}
// copy an emoji from remote to local
func (p *Processor) emojiUpdateCopy(ctx context.Context, emoji *gtsmodel.Emoji, shortcode *string, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain == "" {
err := fmt.Errorf("emojiUpdateCopy: emoji %s is not a remote emoji, cannot copy it to local", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
if shortcode == nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, no shortcode provided", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, *shortcode, "")
if maybeExisting != nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, emoji with shortcode %s already exists on this instance", emoji.ID, *shortcode)
return nil, gtserror.NewErrorConflict(err, err.Error())
}
if err != nil && err != db.ErrNoEntries {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error checking existence of emoji with shortcode %s: %s", emoji.ID, *shortcode, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmojiID, err := id.NewRandomULID()
if err != nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error creating id for new emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmojiURI := uris.GenerateURIForEmoji(newEmojiID)
data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
rc, err := p.storage.GetStream(ctx, emoji.ImagePath)
return rc, int64(emoji.ImageFileSize), err
}
var ai *media.AdditionalEmojiInfo
if categoryName != nil {
category, err := p.getOrCreateEmojiCategory(ctx, *categoryName)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error getting or creating category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
ai = &media.AdditionalEmojiInfo{
CategoryID: &category.ID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, *shortcode, newEmojiID, newEmojiURI, ai, false)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error processing emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmoji, err := processingEmoji.LoadEmoji(ctx)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error loading processed emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, newEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// disable a remote emoji
func (p *Processor) emojiUpdateDisable(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain == "" {
err := fmt.Errorf("emojiUpdateDisable: emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
emojiDisabled := true
emoji.Disabled = &emojiDisabled
updatedEmoji, err := p.db.UpdateEmoji(ctx, emoji, "updated_at", "disabled")
if err != nil {
err = fmt.Errorf("emojiUpdateDisable: error updating emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateDisable: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// modify a local emoji
func (p *Processor) emojiUpdateModify(ctx context.Context, emoji *gtsmodel.Emoji, image *multipart.FileHeader, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain != "" {
err := fmt.Errorf("emojiUpdateModify: emoji %s is not a local emoji, cannot do a modify action on it", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
var updatedEmoji *gtsmodel.Emoji
// keep existing categoryID unless a new one is defined
var (
updatedCategoryID = emoji.CategoryID
updateCategoryID bool
)
if categoryName != nil {
category, err := p.getOrCreateEmojiCategory(ctx, *categoryName)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error getting or creating category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
updatedCategoryID = category.ID
updateCategoryID = true
}
// only update image if provided with one
var updateImage bool
if image != nil && image.Size != 0 {
updateImage = true
}
if !updateImage {
// only updating fields, we only need
// to do a database update for this
columns := []string{"updated_at"}
if updateCategoryID {
emoji.CategoryID = updatedCategoryID
columns = append(columns, "category_id")
}
var err error
updatedEmoji, err = p.db.UpdateEmoji(ctx, emoji, columns...)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error updating emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
} else {
// new image, so we need to reprocess the emoji
data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
i, err := image.Open()
return i, image.Size, err
}
var ai *media.AdditionalEmojiInfo
if updateCategoryID {
ai = &media.AdditionalEmojiInfo{
CategoryID: &updatedCategoryID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, emoji.Shortcode, emoji.ID, emoji.URI, ai, true)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error processing emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
updatedEmoji, err = processingEmoji.LoadEmoji(ctx)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error loading processed emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}

View File

@@ -1,60 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
)
func (p *processor) GetOrCreateEmojiCategory(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) {
category, err := p.db.GetEmojiCategoryByName(ctx, name)
if err == nil {
return category, nil
}
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("GetOrCreateEmojiCategory: database error trying get emoji category by name: %s", err)
return nil, err
}
// we don't have the category yet, just create it with the given name
categoryID, err := id.NewRandomULID()
if err != nil {
err = fmt.Errorf("GetOrCreateEmojiCategory: error generating id for new emoji category: %s", err)
return nil, err
}
category = &gtsmodel.EmojiCategory{
ID: categoryID,
Name: name,
}
if err := p.db.PutEmojiCategory(ctx, category); err != nil {
err = fmt.Errorf("GetOrCreateEmojiCategory: error putting new emoji category in the database: %s", err)
return nil, err
}
return category, nil
}

View File

@@ -1,47 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (p *processor) EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) {
categories, err := p.db.GetEmojiCategories(ctx)
if err != nil {
err := fmt.Errorf("EmojiCategoriesGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories))
for _, category := range categories {
apiCategory, err := p.tc.EmojiCategoryToAPIEmojiCategory(ctx, category)
if err != nil {
err := fmt.Errorf("EmojiCategoriesGet: error converting emoji category to api emoji category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiCategories = append(apiCategories, apiCategory)
}
return apiCategories, nil
}

View File

@@ -1,49 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(ctx, id, domainBlock); err != nil {
if err != db.ErrNoEntries {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
// there are no entries for this ID
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
}
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apiDomainBlock, nil
}

View File

@@ -1,50 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
domainBlocks := []*gtsmodel.DomainBlock{}
if err := p.db.GetAll(ctx, &domainBlocks); err != nil {
if err != db.ErrNoEntries {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
}
apiDomainBlocks := []*apimodel.DomainBlock{}
for _, b := range domainBlocks {
apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, b, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock)
}
return apiDomainBlocks, nil
}

View File

@@ -1,54 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}

View File

@@ -1,97 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
"strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) EmojisGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
if !*user.Admin {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
emojis, err := p.db.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := fmt.Errorf("EmojisGet: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(emojis)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
items := make([]interface{}, 0, count)
for _, emoji := range emojis {
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
items = append(items, adminEmoji)
}
filterBuilder := strings.Builder{}
filterBuilder.WriteString("filter=")
switch domain {
case "", "local":
filterBuilder.WriteString("domain:local")
case db.EmojiAllDomains:
filterBuilder.WriteString("domain:all")
default:
filterBuilder.WriteString("domain:")
filterBuilder.WriteString(domain)
}
if includeDisabled != includeEnabled {
if includeDisabled {
filterBuilder.WriteString(",disabled")
}
if includeEnabled {
filterBuilder.WriteString(",enabled")
}
}
if shortcode != "" {
filterBuilder.WriteString(",shortcode:")
filterBuilder.WriteString(shortcode)
}
return util.PackagePageableResponse(util.PageableResponseParams{
Items: items,
Path: "api/v1/admin/custom_emojis",
NextMaxIDKey: "max_shortcode_domain",
NextMaxIDValue: util.ShortcodeDomain(emojis[count-1]),
PrevMinIDKey: "min_shortcode_domain",
PrevMinIDValue: util.ShortcodeDomain(emojis[0]),
Limit: limit,
ExtraQueryParams: []string{filterBuilder.String()},
})
}

View File

@@ -1,45 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, report, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apimodelReport, nil
}

View File

@@ -1,66 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file.
func (p *processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) {
f, err := domains.Open()
if err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err))
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error reading attachment: %s", err))
}
if size == 0 {
return nil, gtserror.NewErrorBadRequest(errors.New("DomainBlocksImport: could not read provided attachment: size 0 bytes"))
}
d := []apimodel.DomainBlock{}
if err := json.Unmarshal(buf.Bytes(), &d); err != nil {
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: could not read provided attachment: %s", err))
}
blocks := []*apimodel.DomainBlock{}
for _, d := range d {
block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "")
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}

View File

@@ -27,7 +27,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/log"
)
func (p *processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
// MediaRefetch forces a refetch of remote emojis.
func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username)
if err != nil {
err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
@@ -46,3 +47,18 @@ func (p *processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmode
return nil
}
// MediaPrune triggers a non-blocking prune of remote media, local unused media, etc.
func (p *Processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode {
if mediaRemoteCacheDays < 0 {
err := fmt.Errorf("MediaPrune: invalid value for mediaRemoteCacheDays prune: value was %d, cannot be less than 0", mediaRemoteCacheDays)
return gtserror.NewErrorBadRequest(err, err.Error())
}
if err := p.mediaManager.PruneAll(ctx, mediaRemoteCacheDays, false); err != nil {
err = fmt.Errorf("MediaPrune: %w", err)
return gtserror.NewErrorInternalError(err)
}
return nil
}

View File

@@ -22,6 +22,7 @@ import (
"context"
"fmt"
"strconv"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
@@ -30,7 +31,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) ReportsGet(
// ReportsGet returns all reports stored on this instance, with the given parameters.
func (p *Processor) ReportsGet(
ctx context.Context,
account *gtsmodel.Account,
resolved *bool,
@@ -90,3 +92,57 @@ func (p *processor) ReportsGet(
ExtraQueryParams: extraQueryParams,
})
}
// ReportGet returns one report, with the given ID.
func (p *Processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, report, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apimodelReport, nil
}
// ReportResolve marks a report with the given id as resolved, and stores the provided actionTakenComment (if not null).
func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
columns := []string{
"action_taken_at",
"action_taken_by_account_id",
}
report.ActionTakenAt = time.Now()
report.ActionTakenByAccountID = account.ID
if actionTakenComment != nil {
report.ActionTaken = *actionTakenComment
columns = append(columns, "action_taken")
}
updatedReport, err := p.db.UpdateReport(ctx, report, columns...)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apimodelReport, nil
}

View File

@@ -1,64 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
columns := []string{
"action_taken_at",
"action_taken_by_account_id",
}
report.ActionTakenAt = time.Now()
report.ActionTakenByAccountID = account.ID
if actionTakenComment != nil {
report.ActionTaken = *actionTakenComment
columns = append(columns, "action_taken")
}
updatedReport, err := p.db.UpdateReport(ctx, report, columns...)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apimodelReport, nil
}

View File

@@ -1,236 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) {
emoji, err := p.db.GetEmojiByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("EmojiUpdate: no emoji with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("EmojiUpdate: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
switch form.Type {
case apimodel.EmojiUpdateCopy:
return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
case apimodel.EmojiUpdateDisable:
return p.emojiUpdateDisable(ctx, emoji)
case apimodel.EmojiUpdateModify:
return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
default:
err := errors.New("unrecognized emoji action type")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
}
// copy an emoji from remote to local
func (p *processor) emojiUpdateCopy(ctx context.Context, emoji *gtsmodel.Emoji, shortcode *string, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain == "" {
err := fmt.Errorf("emojiUpdateCopy: emoji %s is not a remote emoji, cannot copy it to local", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
if shortcode == nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, no shortcode provided", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, *shortcode, "")
if maybeExisting != nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, emoji with shortcode %s already exists on this instance", emoji.ID, *shortcode)
return nil, gtserror.NewErrorConflict(err, err.Error())
}
if err != nil && err != db.ErrNoEntries {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error checking existence of emoji with shortcode %s: %s", emoji.ID, *shortcode, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmojiID, err := id.NewRandomULID()
if err != nil {
err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error creating id for new emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmojiURI := uris.GenerateURIForEmoji(newEmojiID)
data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
rc, err := p.storage.GetStream(ctx, emoji.ImagePath)
return rc, int64(emoji.ImageFileSize), err
}
var ai *media.AdditionalEmojiInfo
if categoryName != nil {
category, err := p.GetOrCreateEmojiCategory(ctx, *categoryName)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error getting or creating category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
ai = &media.AdditionalEmojiInfo{
CategoryID: &category.ID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, *shortcode, newEmojiID, newEmojiURI, ai, false)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error processing emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
newEmoji, err := processingEmoji.LoadEmoji(ctx)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error loading processed emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, newEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateCopy: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// disable a remote emoji
func (p *processor) emojiUpdateDisable(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain == "" {
err := fmt.Errorf("emojiUpdateDisable: emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
emojiDisabled := true
emoji.Disabled = &emojiDisabled
updatedEmoji, err := p.db.UpdateEmoji(ctx, emoji, "updated_at", "disabled")
if err != nil {
err = fmt.Errorf("emojiUpdateDisable: error updating emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateDisable: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
// modify a local emoji
func (p *processor) emojiUpdateModify(ctx context.Context, emoji *gtsmodel.Emoji, image *multipart.FileHeader, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) {
if emoji.Domain != "" {
err := fmt.Errorf("emojiUpdateModify: emoji %s is not a local emoji, cannot do a modify action on it", emoji.ID)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
var updatedEmoji *gtsmodel.Emoji
// keep existing categoryID unless a new one is defined
var (
updatedCategoryID = emoji.CategoryID
updateCategoryID bool
)
if categoryName != nil {
category, err := p.GetOrCreateEmojiCategory(ctx, *categoryName)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error getting or creating category: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
updatedCategoryID = category.ID
updateCategoryID = true
}
// only update image if provided with one
var updateImage bool
if image != nil && image.Size != 0 {
updateImage = true
}
if !updateImage {
// only updating fields, we only need
// to do a database update for this
columns := []string{"updated_at"}
if updateCategoryID {
emoji.CategoryID = updatedCategoryID
columns = append(columns, "category_id")
}
var err error
updatedEmoji, err = p.db.UpdateEmoji(ctx, emoji, columns...)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error updating emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
} else {
// new image, so we need to reprocess the emoji
data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) {
i, err := image.Open()
return i, image.Size, err
}
var ai *media.AdditionalEmojiInfo
if updateCategoryID {
ai = &media.AdditionalEmojiInfo{
CategoryID: &updatedCategoryID,
}
}
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, emoji.Shortcode, emoji.ID, emoji.URI, ai, true)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error processing emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
updatedEmoji, err = processingEmoji.LoadEmoji(ctx)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error loading processed emoji %s: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
}
adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji)
if err != nil {
err = fmt.Errorf("emojiUpdateModify: error converting updated emoji %s to admin emoji: %s", emoji.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}

View File

@@ -29,7 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
func (p *Processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
// set default 'read' for scopes if it's not set
var scopes string
if form.Scopes == "" {

View File

@@ -30,7 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
func (p *Processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
accounts, nextMaxID, prevMinID, err := p.db.GetAccountBlocks(ctx, authed.Account.ID, maxID, sinceID, limit)
if err != nil {
if err == db.ErrNoEntries {
@@ -55,7 +55,7 @@ func (p *processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID str
return p.packageBlocksResponse(apiAccounts, "/api/v1/blocks", nextMaxID, prevMinID, limit)
}
func (p *processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
func (p *Processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
resp := &apimodel.BlocksResponse{
Accounts: []*apimodel.Account{},
}

View File

@@ -1,31 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.accountProcessor.BookmarksGet(ctx, authed.Account, limit, maxID, minID)
}

View File

@@ -1,72 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
"net/http"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetUser(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetFollowers(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetFollowing(ctx, requestedUsername, requestURL)
}
func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetStatus(ctx, requestedUsername, requestedStatusID, requestURL)
}
func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, requestURL)
}
func (p *processor) GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL)
}
func (p *processor) GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
return p.federationProcessor.GetEmoji(ctx, requestedEmojiID, requestURL)
}
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername)
}
func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) {
return p.federationProcessor.GetNodeInfoRel(ctx)
}
func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) {
return p.federationProcessor.GetNodeInfo(ctx)
}
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return p.federationProcessor.PostInbox(ctx, w, r)
}

View File

@@ -1,100 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"net/http"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
// Processor wraps functions for processing federation API requests.
type Processor interface {
// GetUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
// before returning a JSON serializable interface to the caller.
GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetFediEmoji handles the GET for a federated emoji originating from this instance.
GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetNodeInfoRel returns a well known response giving the path to node info.
GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetNodeInfo returns a node info struct in response to a node info request.
GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode)
// GetOutbox returns the activitypub representation of a local user's outbox.
// This contains links to PUBLIC posts made by this user.
GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// PostInbox handles POST requests to a user's inbox for new activitypub messages.
//
// PostInbox returns true if the request was handled as an ActivityPub POST to an actor's inbox.
// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
//
// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
//
// If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
//
// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}
type processor struct {
db db.DB
federator federation.Federator
tc typeutils.TypeConverter
filter visibility.Filter
}
// New returns a new federation processor.
func New(db db.DB, tc typeutils.TypeConverter, federator federation.Federator) Processor {
return &processor{
db: db,
federator: federator,
tc: tc,
filter: visibility.NewFilter(db),
}
}

View File

@@ -1,76 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
}
requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
}
data, err := streams.Serialize(requestedFollowers)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}

View File

@@ -1,76 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
}
requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
}
data, err := streams.Serialize(requestedFollowing)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}

View File

@@ -1,89 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
const (
nodeInfoVersion = "2.0"
nodeInfoSoftwareName = "gotosocial"
)
var (
nodeInfoRel = fmt.Sprintf("http://nodeinfo.diaspora.software/ns/schema/%s", nodeInfoVersion)
nodeInfoProtocols = []string{"activitypub"}
)
func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) {
protocol := config.GetProtocol()
host := config.GetHost()
return &apimodel.WellKnownResponse{
Links: []apimodel.Link{
{
Rel: nodeInfoRel,
Href: fmt.Sprintf("%s://%s/nodeinfo/%s", protocol, host, nodeInfoVersion),
},
},
}, nil
}
func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) {
openRegistration := config.GetAccountsRegistrationOpen()
softwareVersion := config.GetSoftwareVersion()
host := config.GetHost()
userCount, err := p.db.CountInstanceUsers(ctx, host)
if err != nil {
return nil, gtserror.NewErrorInternalError(err, "Unable to query instance user count")
}
postCount, err := p.db.CountInstanceStatuses(ctx, host)
if err != nil {
return nil, gtserror.NewErrorInternalError(err, "Unable to query instance status count")
}
return &apimodel.Nodeinfo{
Version: nodeInfoVersion,
Software: apimodel.NodeInfoSoftware{
Name: nodeInfoSoftwareName,
Version: softwareVersion,
},
Protocols: nodeInfoProtocols,
Services: apimodel.NodeInfoServices{
Inbound: []string{},
Outbound: []string{},
},
OpenRegistrations: openRegistration,
Usage: apimodel.NodeInfoUsage{
Users: apimodel.NodeInfoUsers{
Total: userCount,
},
LocalPosts: postCount,
},
Metadata: make(map[string]interface{}),
}, nil
}

View File

@@ -1,109 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
// 1. check if a block exists between the requester and the requestee
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
var data map[string]interface{}
// now there are two scenarios:
// 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
// 2. we're asked for a specific page; this can be either the first page or any other page
if !page {
/*
scenario 1: return the collection with no items
we want something that looks like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.org/users/whatever/outbox",
"type": "OrderedCollection",
"first": "https://example.org/users/whatever/outbox?page=true",
"last": "https://example.org/users/whatever/outbox?min_id=0&page=true"
}
*/
collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err = streams.Serialize(collection)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// scenario 2 -- get the requested page
// limit pages to 30 entries per page
publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true)
if err != nil && err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(err)
}
outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err = streams.Serialize(outboxPage)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}

View File

@@ -1,92 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
// 1. check if a block exists between the requester and the requestee
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
// get the status out of the database here
s, err := p.db.GetStatusByID(ctx, requestedStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
}
if s.AccountID != requestedAccount.ID {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s does not belong to account with id %s", s.ID, requestedAccount.ID))
}
visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if !visible {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
}
// requester is authorized to view the status, so convert it to AP representation and serialize it
asStatus, err := p.tc.StatusToAS(ctx, s)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err := streams.Serialize(asStatus)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}

View File

@@ -1,70 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
const (
webfingerProfilePage = "http://webfinger.net/rel/profile-page"
webFingerProfilePageContentType = "text/html"
webfingerSelf = "self"
webFingerSelfContentType = "application/activity+json"
webfingerAccount = "acct"
)
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
accountDomain := config.GetAccountDomain()
if accountDomain == "" {
accountDomain = config.GetHost()
}
// return the webfinger representation
return &apimodel.WellKnownResponse{
Subject: fmt.Sprintf("%s:%s@%s", webfingerAccount, requestedAccount.Username, accountDomain),
Aliases: []string{
requestedAccount.URI,
requestedAccount.URL,
},
Links: []apimodel.Link{
{
Rel: webfingerProfilePage,
Type: webFingerProfilePageContentType,
Href: requestedAccount.URL,
},
{
Rel: webfingerSelf,
Type: webFingerSelfContentType,
Href: requestedAccount.URI,
},
},
}, nil
}

View File

@@ -1,28 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 federation
import (
"context"
"net/http"
)
func (p *processor) PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return p.federator.FederatingActor().PostInbox(ctx, w, r)
}

View File

@@ -0,0 +1,224 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 fedi
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
// FollowersGet handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
func (p *Processor) FollowersGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
}
requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
}
data, err := streams.Serialize(requestedFollowers)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// FollowingGet handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
func (p *Processor) FollowingGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
}
requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
}
data, err := streams.Serialize(requestedFollowing)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// OutboxGet returns the activitypub representation of a local user's outbox.
// This contains links to PUBLIC posts made by this user.
func (p *Processor) OutboxGet(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
// 1. check if a block exists between the requester and the requestee
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
var data map[string]interface{}
// now there are two scenarios:
// 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page.
// 2. we're asked for a specific page; this can be either the first page or any other page
if !page {
/*
scenario 1: return the collection with no items
we want something that looks like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.org/users/whatever/outbox",
"type": "OrderedCollection",
"first": "https://example.org/users/whatever/outbox?page=true",
"last": "https://example.org/users/whatever/outbox?min_id=0&page=true"
}
*/
collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err = streams.Serialize(collection)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// scenario 2 -- get the requested page
// limit pages to 30 entries per page
publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true)
if err != nil && err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(err)
}
outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err = streams.Serialize(outboxPage)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
//
// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox.
// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
//
// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
//
// If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
//
// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
func (p *Processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return p.federator.FederatingActor().PostInbox(ctx, w, r)
}

View File

@@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package federation
package fedi
import (
"context"
@@ -27,7 +27,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (p *processor) GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// EmojiGet handles the GET for a federated emoji originating from this instance.
func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil {
return nil, errWithCode
}

View File

@@ -16,25 +16,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package admin
package fedi
import (
"context"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
func (p *processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode {
if mediaRemoteCacheDays < 0 {
err := fmt.Errorf("MediaPrune: invalid value for mediaRemoteCacheDays prune: value was %d, cannot be less than 0", mediaRemoteCacheDays)
return gtserror.NewErrorBadRequest(err, err.Error())
}
if err := p.mediaManager.PruneAll(ctx, mediaRemoteCacheDays, false); err != nil {
err = fmt.Errorf("MediaPrune: %w", err)
return gtserror.NewErrorInternalError(err)
}
return nil
type Processor struct {
db db.DB
federator federation.Federator
tc typeutils.TypeConverter
filter visibility.Filter
}
// New returns a new fedi processor.
func New(db db.DB, tc typeutils.TypeConverter, federator federation.Federator) Processor {
return Processor{
db: db,
federator: federator,
tc: tc,
filter: visibility.NewFilter(db),
}
}

View File

@@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package federation
package fedi
import (
"context"
@@ -30,7 +30,74 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// StatusGet handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
func (p *Processor) StatusGet(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// authenticate the request
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.federator.GetAccountByURI(
transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
// 1. check if a block exists between the requester and the requestee
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
// get the status out of the database here
s, err := p.db.GetStatusByID(ctx, requestedStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
}
if s.AccountID != requestedAccount.ID {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s does not belong to account with id %s", s.ID, requestedAccount.ID))
}
visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if !visible {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID))
}
// requester is authorized to view the status, so convert it to AP representation and serialize it
asStatus, err := p.tc.StatusToAS(ctx, s)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
data, err := streams.Serialize(asStatus)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}
// GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
func (p *Processor) StatusRepliesGet(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {

View File

@@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package federation
package fedi
import (
"context"
@@ -30,7 +30,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// UserGet handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
// before returning a JSON serializable interface to the caller.
func (p *Processor) UserGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// Get the instance-local account the request is referring to.
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {

View File

@@ -0,0 +1,126 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 fedi
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
const (
nodeInfoVersion = "2.0"
nodeInfoSoftwareName = "gotosocial"
nodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/" + nodeInfoVersion
webfingerProfilePage = "http://webfinger.net/rel/profile-page"
webFingerProfilePageContentType = "text/html"
webfingerSelf = "self"
webFingerSelfContentType = "application/activity+json"
webfingerAccount = "acct"
)
var (
nodeInfoProtocols = []string{"activitypub"}
nodeInfoInbound = []string{}
nodeInfoOutbound = []string{}
nodeInfoMetadata = make(map[string]interface{})
)
// NodeInfoRelGet returns a well known response giving the path to node info.
func (p *Processor) NodeInfoRelGet(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) {
protocol := config.GetProtocol()
host := config.GetHost()
return &apimodel.WellKnownResponse{
Links: []apimodel.Link{
{
Rel: nodeInfoRel,
Href: fmt.Sprintf("%s://%s/nodeinfo/%s", protocol, host, nodeInfoVersion),
},
},
}, nil
}
// NodeInfoGet returns a node info struct in response to a node info request.
func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) {
host := config.GetHost()
userCount, err := p.db.CountInstanceUsers(ctx, host)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
postCount, err := p.db.CountInstanceStatuses(ctx, host)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return &apimodel.Nodeinfo{
Version: nodeInfoVersion,
Software: apimodel.NodeInfoSoftware{
Name: nodeInfoSoftwareName,
Version: config.GetSoftwareVersion(),
},
Protocols: nodeInfoProtocols,
Services: apimodel.NodeInfoServices{
Inbound: nodeInfoInbound,
Outbound: nodeInfoOutbound,
},
OpenRegistrations: config.GetAccountsRegistrationOpen(),
Usage: apimodel.NodeInfoUsage{
Users: apimodel.NodeInfoUsers{
Total: userCount,
},
LocalPosts: postCount,
},
Metadata: nodeInfoMetadata,
}, nil
}
// WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// Get the local account the request is referring to.
requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "")
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
return &apimodel.WellKnownResponse{
Subject: webfingerAccount + ":" + requestedAccount.Username + "@" + config.GetAccountDomain(),
Aliases: []string{
requestedAccount.URI,
requestedAccount.URL,
},
Links: []apimodel.Link{
{
Rel: webfingerProfilePage,
Type: webFingerProfilePageContentType,
Href: requestedAccount.URL,
},
{
Rel: webfingerSelf,
Type: webFingerSelfContentType,
Href: requestedAccount.URI,
},
},
}, nil
}

View File

@@ -29,7 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
frs, err := p.db.GetAccountFollowRequests(ctx, auth.Account.ID)
if err != nil {
if err != db.ErrNoEntries {
@@ -56,7 +56,7 @@ func (p *processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]
return accts, nil
}
func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
follow, err := p.db.AcceptFollowRequest(ctx, accountID, auth.Account.ID)
if err != nil {
return nil, gtserror.NewErrorNotFound(err)
@@ -99,7 +99,7 @@ func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a
return r, nil
}
func (p *processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
followRequest, err := p.db.RejectFollowRequest(ctx, accountID, auth.Account.ID)
if err != nil {
return nil, gtserror.NewErrorNotFound(err)

View File

@@ -34,7 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
// Allocate new log fields slice
fields := make([]kv.Field, 3, 4)
fields[0] = kv.Field{"activityType", clientMsg.APActivityType}
@@ -131,7 +131,7 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages
return nil
}
func (p *processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return errors.New("account was not parseable as *gtsmodel.Account")
@@ -149,10 +149,10 @@ func (p *processor) processCreateAccountFromClientAPI(ctx context.Context, clien
}
// email a confirmation to this user
return p.userProcessor.SendConfirmEmail(ctx, user, account.Username)
return p.User().EmailSendConfirmation(ctx, user, account.Username)
}
func (p *processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
@@ -169,7 +169,7 @@ func (p *processor) processCreateStatusFromClientAPI(ctx context.Context, client
return p.federateStatus(ctx, status)
}
func (p *processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest")
@@ -182,7 +182,7 @@ func (p *processor) processCreateFollowRequestFromClientAPI(ctx context.Context,
return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return errors.New("fave was not parseable as *gtsmodel.StatusFave")
@@ -195,7 +195,7 @@ func (p *processor) processCreateFaveFromClientAPI(ctx context.Context, clientMs
return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("boost was not parseable as *gtsmodel.Status")
@@ -212,7 +212,7 @@ func (p *processor) processCreateAnnounceFromClientAPI(ctx context.Context, clie
return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("block was not parseable as *gtsmodel.Block")
@@ -232,7 +232,7 @@ func (p *processor) processCreateBlockFromClientAPI(ctx context.Context, clientM
return p.federateBlock(ctx, block)
}
func (p *processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return errors.New("account was not parseable as *gtsmodel.Account")
@@ -241,7 +241,7 @@ func (p *processor) processUpdateAccountFromClientAPI(ctx context.Context, clien
return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount)
}
func (p *processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
return errors.New("accept was not parseable as *gtsmodel.Follow")
@@ -254,7 +254,7 @@ func (p *processor) processAcceptFollowFromClientAPI(ctx context.Context, client
return p.federateAcceptFollowRequest(ctx, follow)
}
func (p *processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return errors.New("reject was not parseable as *gtsmodel.FollowRequest")
@@ -263,7 +263,7 @@ func (p *processor) processRejectFollowFromClientAPI(ctx context.Context, client
return p.federateRejectFollowRequest(ctx, followRequest)
}
func (p *processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.Follow")
@@ -271,7 +271,7 @@ func (p *processor) processUndoFollowFromClientAPI(ctx context.Context, clientMs
return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.Block")
@@ -279,7 +279,7 @@ func (p *processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg
return p.federateUnblock(ctx, block)
}
func (p *processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.StatusFave")
@@ -287,7 +287,7 @@ func (p *processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg
return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
boost, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.Status")
@@ -304,7 +304,7 @@ func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, client
return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
func (p *processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
@@ -326,7 +326,7 @@ func (p *processor) processDeleteStatusFromClientAPI(ctx context.Context, client
return p.federateStatusDelete(ctx, statusToDelete)
}
func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
// the origin of the delete could be either a domain block, or an action by another (or this) account
var origin string
if domainBlock, ok := clientMsg.GTSModel.(*gtsmodel.DomainBlock); ok {
@@ -341,10 +341,10 @@ func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clien
return err
}
return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin)
return p.account.Delete(ctx, clientMsg.TargetAccount, origin)
}
func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
report, ok := clientMsg.GTSModel.(*gtsmodel.Report)
if !ok {
return errors.New("report was not parseable as *gtsmodel.Report")
@@ -362,7 +362,7 @@ func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clien
// TODO: move all the below functions into federation.Federator
func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
// do nothing if this isn't our account
if account.Domain != "" {
return nil
@@ -415,7 +415,7 @@ func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel
return err
}
func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error {
// do nothing if the status shouldn't be federated
if !*status.Federated {
return nil
@@ -453,7 +453,7 @@ func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status)
return err
}
func (p *processor) federateStatusDelete(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.Status) error {
if status.Account == nil {
statusAccount, err := p.db.GetAccountByID(ctx, status.AccountID)
if err != nil {
@@ -503,7 +503,7 @@ func (p *processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S
return err
}
func (p *processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
return nil
@@ -525,7 +525,7 @@ func (p *processor) federateFollow(ctx context.Context, followRequest *gtsmodel.
return err
}
func (p *processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
return nil
@@ -566,7 +566,7 @@ func (p *processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follo
return err
}
func (p *processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
return nil
@@ -605,7 +605,7 @@ func (p *processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFav
return err
}
func (p *processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
if originAccount.Domain != "" {
// nothing to do here
return nil
@@ -640,7 +640,7 @@ func (p *processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Stat
return err
}
func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error {
func (p *Processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error {
if follow.Account == nil {
a, err := p.db.GetAccountByID(ctx, follow.AccountID)
if err != nil {
@@ -713,7 +713,7 @@ func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gts
return err
}
func (p *processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
func (p *Processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
if followRequest.Account == nil {
a, err := p.db.GetAccountByID(ctx, followRequest.AccountID)
if err != nil {
@@ -787,7 +787,7 @@ func (p *processor) federateRejectFollowRequest(ctx context.Context, followReque
return err
}
func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
return nil
@@ -807,7 +807,7 @@ func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave,
return err
}
func (p *processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error {
func (p *Processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error {
announce, err := p.tc.BoostToAS(ctx, boostWrapperStatus, boostingAccount, boostedAccount)
if err != nil {
return fmt.Errorf("federateAnnounce: error converting status to announce: %s", err)
@@ -822,7 +822,7 @@ func (p *processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gt
return err
}
func (p *processor) federateAccountUpdate(ctx context.Context, updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error {
func (p *Processor) federateAccountUpdate(ctx context.Context, updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error {
person, err := p.tc.AccountToAS(ctx, updatedAccount)
if err != nil {
return fmt.Errorf("federateAccountUpdate: error converting account to person: %s", err)
@@ -842,7 +842,7 @@ func (p *processor) federateAccountUpdate(ctx context.Context, updatedAccount *g
return err
}
func (p *processor) federateBlock(ctx context.Context, block *gtsmodel.Block) error {
func (p *Processor) federateBlock(ctx context.Context, block *gtsmodel.Block) error {
if block.Account == nil {
blockAccount, err := p.db.GetAccountByID(ctx, block.AccountID)
if err != nil {
@@ -878,7 +878,7 @@ func (p *processor) federateBlock(ctx context.Context, block *gtsmodel.Block) er
return err
}
func (p *processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) error {
func (p *Processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) error {
if block.Account == nil {
blockAccount, err := p.db.GetAccountByID(ctx, block.AccountID)
if err != nil {
@@ -932,7 +932,7 @@ func (p *processor) federateUnblock(ctx context.Context, block *gtsmodel.Block)
return err
}
func (p *processor) federateReport(ctx context.Context, report *gtsmodel.Report) error {
func (p *Processor) federateReport(ctx context.Context, report *gtsmodel.Report) error {
if report.TargetAccount == nil {
reportTargetAccount, err := p.db.GetAccountByID(ctx, report.TargetAccountID)
if err != nil {

View File

@@ -46,12 +46,12 @@ func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() {
receivingAccount := suite.testAccounts["local_account_1"]
// open a home timeline stream for zork
wssStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// open another stream for zork, but for a different timeline;
// this shouldn't get stuff streamed into it, since it's for the public timeline
irrelevantStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelinePublic)
irrelevantStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelinePublic)
suite.NoError(errWithCode)
// make a new status from admin account
@@ -125,7 +125,7 @@ func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
boostOfDeletedStatus := suite.testStatuses["admin_account_status_4"]
// open a home timeline stream for turtle, who follows zork
wssStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// delete the status from the db first, to mimic what would have already happened earlier up the flow

View File

@@ -30,7 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/stream"
)
func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) error {
// if there are no mentions in this status then just bail
if len(status.MentionIDs) == 0 {
return nil
@@ -97,7 +97,7 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e
return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err)
}
if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, m.TargetAccount); err != nil {
if err := p.stream.Notify(apiNotif, m.TargetAccount); err != nil {
return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
}
}
@@ -105,7 +105,7 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e
return nil
}
func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
// make sure we have the target account pinned on the follow request
if followRequest.TargetAccount == nil {
a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID)
@@ -139,14 +139,14 @@ func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsm
return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err)
}
if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil {
if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
}
return nil
}
func (p *processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error {
func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error {
// return if this isn't a local account
if targetAccount.Domain != "" {
return nil
@@ -180,14 +180,14 @@ func (p *processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, t
return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err)
}
if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil {
if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
}
return nil
}
func (p *processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
// ignore self-faves
if fave.TargetAccountID == fave.AccountID {
return nil
@@ -228,14 +228,14 @@ func (p *processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) e
return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err)
}
if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil {
if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
}
return nil
}
func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error {
if status.BoostOfID == "" {
// not a boost, nothing to do
return nil
@@ -302,7 +302,7 @@ func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status)
return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err)
}
if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, status.BoostOfAccount); err != nil {
if err := p.stream.Notify(apiNotif, status.BoostOfAccount); err != nil {
return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
}
@@ -311,7 +311,7 @@ func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status)
// timelineStatus processes the given new status and inserts it into
// the HOME timelines of accounts that follow the status author.
func (p *processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error {
// make sure the author account is pinned onto the status
if status.Account == nil {
a, err := p.db.GetAccountByID(ctx, status.AccountID)
@@ -370,7 +370,7 @@ func (p *processor) timelineStatus(ctx context.Context, status *gtsmodel.Status)
//
// If the status was inserted into the home timeline of the given account,
// it will also be streamed via websockets to the user.
func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) {
func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) {
defer wg.Done()
// get the timeline owner account
@@ -406,7 +406,7 @@ func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmod
return
}
if err := p.streamingProcessor.StreamUpdateToAccount(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error streaming status %s: %s", status.ID, err)
}
}
@@ -414,17 +414,17 @@ func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmod
// deleteStatusFromTimelines completely removes the given status from all timelines.
// It will also stream deletion of the status to all open streams.
func (p *processor) deleteStatusFromTimelines(ctx context.Context, status *gtsmodel.Status) error {
func (p *Processor) deleteStatusFromTimelines(ctx context.Context, status *gtsmodel.Status) error {
if err := p.statusTimelines.WipeItemFromAllTimelines(ctx, status.ID); err != nil {
return err
}
return p.streamingProcessor.StreamDelete(status.ID)
return p.stream.Delete(status.ID)
}
// wipeStatus contains common logic used to totally delete a status
// + all its attachments, notifications, boosts, and timeline entries.
func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error {
func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error {
// either delete all attachments for this status, or simply
// unattach all attachments for this status, so they'll be
// cleaned later by a separate process; reason to unattach rather
@@ -432,13 +432,13 @@ func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
// to another status immediately (in case of delete + redraft)
if deleteAttachments {
for _, a := range statusToDelete.AttachmentIDs {
if err := p.mediaProcessor.Delete(ctx, a); err != nil {
if err := p.media.Delete(ctx, a); err != nil {
return err
}
}
} else {
for _, a := range statusToDelete.AttachmentIDs {
if _, err := p.mediaProcessor.Unattach(ctx, statusToDelete.Account, a); err != nil {
if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil {
return err
}
}

View File

@@ -36,7 +36,7 @@ import (
// ProcessFromFederator reads the APActivityType and APObjectType of an incoming message from the federator,
// and directs the message into the appropriate side effect handler function, or simply does nothing if there's
// no handler function defined for the combination of Activity and Object.
func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
// Allocate new log fields slice
fields := make([]kv.Field, 3, 5)
fields[0] = kv.Field{"activityType", federatorMsg.APActivityType}
@@ -108,7 +108,7 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
}
// processCreateStatusFromFederator handles Activity Create and Object Note
func (p *processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
// check for either an IRI that we still need to dereference, OR an already dereferenced
// and converted status pinned to the message.
var status *gtsmodel.Status
@@ -177,7 +177,7 @@ func (p *processor) processCreateStatusFromFederator(ctx context.Context, federa
}
// processCreateFaveFromFederator handles Activity Create and Object Like
func (p *processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return errors.New("like was not parseable as *gtsmodel.StatusFave")
@@ -219,7 +219,7 @@ func (p *processor) processCreateFaveFromFederator(ctx context.Context, federato
}
// processCreateFollowRequestFromFederator handles Activity Create and Object Follow
func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest")
@@ -280,7 +280,7 @@ func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context,
}
// processCreateAnnounceFromFederator handles Activity Create and Object Announce
func (p *processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("announce was not parseable as *gtsmodel.Status")
@@ -340,7 +340,7 @@ func (p *processor) processCreateAnnounceFromFederator(ctx context.Context, fede
}
// processCreateBlockFromFederator handles Activity Create and Object Block
func (p *processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("block was not parseable as *gtsmodel.Block")
@@ -359,7 +359,7 @@ func (p *processor) processCreateBlockFromFederator(ctx context.Context, federat
return nil
}
func (p *processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
// TODO: handle side effects of flag creation:
// - send email to admins
// - notify admins
@@ -367,7 +367,7 @@ func (p *processor) processCreateFlagFromFederator(ctx context.Context, federato
}
// processUpdateAccountFromFederator handles Activity Update and Object Profile
func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return errors.New("profile was not parseable as *gtsmodel.Account")
@@ -391,7 +391,7 @@ func (p *processor) processUpdateAccountFromFederator(ctx context.Context, feder
}
// processDeleteStatusFromFederator handles Activity Delete and Object Note
func (p *processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
@@ -405,11 +405,11 @@ func (p *processor) processDeleteStatusFromFederator(ctx context.Context, federa
}
// processDeleteAccountFromFederator handles Activity Delete and Object Profile
func (p *processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
func (p *Processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
account, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return errors.New("account delete was not parseable as *gtsmodel.Account")
}
return p.accountProcessor.Delete(ctx, account, account.ID)
return p.account.Delete(ctx, account, account.ID)
}

View File

@@ -117,7 +117,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
Likeable: testrig.FalseBool(),
}
wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), repliedAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// id the status based on the time it was created
@@ -183,7 +183,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), favedAccount, stream.TimelineNotifications)
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@@ -256,7 +256,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), receivingAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@@ -400,7 +400,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
// target is a locked account
targetAccount := suite.testAccounts["local_account_2"]
wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already
@@ -457,7 +457,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
// target is an unlocked account
targetAccount := suite.testAccounts["local_account_1"]
wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, stream.TimelineHome)
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already

View File

@@ -33,7 +33,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
func (p *processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) {
func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) {
i := &gtsmodel.Instance{}
if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: config.GetHost()}}, i); err != nil {
return nil, err
@@ -41,7 +41,7 @@ func (p *processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, er
return i, nil
}
func (p *processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
i, err := p.getThisInstance(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err))
@@ -55,7 +55,7 @@ func (p *processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gt
return ai, nil
}
func (p *processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) {
func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) {
i, err := p.getThisInstance(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err))
@@ -69,7 +69,7 @@ func (p *processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gt
return ai, nil
}
func (p *processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) {
func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) {
domains := []*apimodel.Domain{}
if includeOpen {
@@ -120,7 +120,7 @@ func (p *processor) InstancePeersGet(ctx context.Context, includeSuspended bool,
return domains, nil
}
func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) {
func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) {
// fetch the instance entry from the db for processing
i := &gtsmodel.Instance{}
host := config.GetHost()
@@ -223,7 +223,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
if form.Avatar != nil && form.Avatar.Size != 0 {
// process instance avatar image + description
avatarInfo, err := p.accountProcessor.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID)
avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
}
@@ -240,7 +240,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
if form.Header != nil && form.Header.Size != 0 {
// process instance header image
headerInfo, err := p.accountProcessor.UpdateHeader(ctx, form.Header, nil, ia.ID)
headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
}

View File

@@ -1,47 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
return p.mediaProcessor.Create(ctx, authed.Account, form)
}
func (p *processor) MediaGet(ctx context.Context, authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
return p.mediaProcessor.GetMedia(ctx, authed.Account, mediaAttachmentID)
}
func (p *processor) MediaUpdate(ctx context.Context, authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
return p.mediaProcessor.Update(ctx, authed.Account, mediaAttachmentID, form)
}
func (p *processor) FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) {
return p.mediaProcessor.GetFile(ctx, authed.Account, form)
}
func (p *processor) CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) {
return p.mediaProcessor.GetCustomEmojis(ctx)
}

View File

@@ -29,7 +29,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/media"
)
func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
// Create creates a new media attachment belonging to the given account, using the request form.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
f, err := form.File.Open()
return f, form.File.Size, err

View File

@@ -11,7 +11,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (p *processor) Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode {
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
func (p *Processor) Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode {
attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -28,7 +28,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/log"
)
func (p *processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) {
// GetCustomEmojis returns a list of all useable local custom emojis stored on this instance.
// 'useable' in this context means visible and picker, and not disabled.
func (p *Processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) {
emojis, err := p.db.GetUseableEmojis(ctx)
if err != nil {
if err != db.ErrNoEntries {

View File

@@ -33,42 +33,15 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized
func parseMediaType(s string) (media.Type, error) {
switch s {
case string(media.TypeAttachment):
return media.TypeAttachment, nil
case string(media.TypeHeader):
return media.TypeHeader, nil
case string(media.TypeAvatar):
return media.TypeAvatar, nil
case string(media.TypeEmoji):
return media.TypeEmoji, nil
}
return "", fmt.Errorf("%s not a recognized media.Type", s)
}
// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized
func parseMediaSize(s string) (media.Size, error) {
switch s {
case string(media.SizeSmall):
return media.SizeSmall, nil
case string(media.SizeOriginal):
return media.SizeOriginal, nil
case string(media.SizeStatic):
return media.SizeStatic, nil
}
return "", fmt.Errorf("%s not a recognized media.Size", s)
}
func (p *processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) {
// GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content.
func (p *Processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) {
// parse the form fields
mediaSize, err := parseMediaSize(form.MediaSize)
mediaSize, err := parseSize(form.MediaSize)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
}
mediaType, err := parseMediaType(form.MediaType)
mediaType, err := parseType(form.MediaType)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
}
@@ -112,7 +85,37 @@ func (p *processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Acc
}
}
func (p *processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) {
/*
UTIL FUNCTIONS
*/
func parseType(s string) (media.Type, error) {
switch s {
case string(media.TypeAttachment):
return media.TypeAttachment, nil
case string(media.TypeHeader):
return media.TypeHeader, nil
case string(media.TypeAvatar):
return media.TypeAvatar, nil
case string(media.TypeEmoji):
return media.TypeEmoji, nil
}
return "", fmt.Errorf("%s not a recognized media.Type", s)
}
func parseSize(s string) (media.Size, error) {
switch s {
case string(media.SizeSmall):
return media.SizeSmall, nil
case string(media.SizeOriginal):
return media.SizeOriginal, nil
case string(media.SizeStatic):
return media.SizeStatic, nil
}
return "", fmt.Errorf("%s not a recognized media.Size", s)
}
func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) {
// retrieve attachment from the database and do basic checks on it
a, err := p.db.GetAttachmentByID(ctx, wantedMediaID)
if err != nil {
@@ -196,7 +199,7 @@ func (p *processor) getAttachmentContent(ctx context.Context, requestingAccount
return p.retrieveFromStorage(ctx, storagePath, attachmentContent)
}
func (p *processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) {
func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) {
emojiContent := &apimodel.Content{}
var storagePath string
@@ -231,7 +234,7 @@ func (p *processor) getEmojiContent(ctx context.Context, fileName string, owning
return p.retrieveFromStorage(ctx, storagePath, emojiContent)
}
func (p *processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) {
func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) {
// If running on S3 storage with proxying disabled then
// just fetch a pre-signed URL instead of serving the content.
if url := p.storage.URL(ctx, storagePath); url != nil {

View File

@@ -29,7 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) GetMedia(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -19,35 +19,14 @@
package media
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// Processor wraps a bunch of functions for processing media actions.
type Processor interface {
// Create creates a new media attachment belonging to the given account, using the request form.
Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode)
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode
// Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available
// for reattachment again.
Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode)
// GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content.
GetFile(ctx context.Context, account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode)
GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode)
GetMedia(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode)
Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
}
type processor struct {
type Processor struct {
tc typeutils.TypeConverter
mediaManager media.Manager
transportController transport.Controller
@@ -57,7 +36,7 @@ type processor struct {
// New returns a new media processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller, storage *storage.Driver) Processor {
return &processor{
return Processor{
tc: tc,
mediaManager: mediaManager,
transportController: transportController,

View File

@@ -30,7 +30,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
// Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available
// for reattachment again.
func (p *Processor) Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -30,7 +30,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
)
func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
// Update updates a media attachment with the given id, using the provided form parameters.
func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) {
func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) {
notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, excludeTypes, limit, maxID, sinceID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
@@ -71,7 +71,7 @@ func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex
})
}
func (p *processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
err := p.db.ClearNotifications(ctx, authed.Account.ID)
if err != nil {
return gtserror.NewErrorInternalError(err)

View File

@@ -25,17 +25,17 @@ import (
"github.com/superseriousbusiness/oauth2/v4"
)
func (p *processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode {
func (p *Processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode {
// todo: some kind of metrics stuff here
return p.oauthServer.HandleAuthorizeRequest(w, r)
}
func (p *processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) {
func (p *Processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) {
// todo: some kind of metrics stuff here
return p.oauthServer.HandleTokenRequest(r)
}
func (p *processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) {
func (p *Processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) {
// todo: some kind of metrics stuff here
return p.oauthServer.ValidationBearerToken(r)
}

View File

@@ -19,291 +19,35 @@
package processing
import (
"context"
"net/http"
"net/url"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
mm "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
federationProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/federation"
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/report"
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
"github.com/superseriousbusiness/oauth2/v4"
)
// Processor should be passed to api modules (see internal/apimodule/...). It is used for
// passing messages back and forth from the client API and the federating interface, via channels.
// It also contains logic for filtering which messages should end up where.
// It is designed to be used asynchronously: the client API and the federating API should just be able to
// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
// for clean distribution of messages without slowing down the client API and harming the user experience.
type Processor interface {
// Start starts the Processor, reading from its channels and passing messages back and forth.
Start() error
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
Stop() error
// ProcessFromClientAPI processes one message coming from the clientAPI channel, and triggers appropriate side effects.
ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error
// ProcessFromFederator processes one message coming from the federator channel, and triggers appropriate side effects.
ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error
/*
CLIENT API-FACING PROCESSING FUNCTIONS
These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply
to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
response, pass work to the processor using a channel instead.
*/
// AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.
AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode)
// AccountDeleteLocal processes the delete of a LOCAL account using the given form.
AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode
// AccountGet processes the given request for account information.
AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
// AccountGet processes the given request for account information.
AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode)
AccountGetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode)
// AccountGetRSSFeedForUsername returns a function to get the RSS feed of latest posts for given local account username.
// This function should only be called if necessary: the given lastModified time can be used to check this.
// Will return 404 if an rss feed for that user is not available, or a different error if something else goes wrong.
AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode)
// AccountUpdate processes the update of an account with the given form
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode)
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode)
// AccountWebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
// statuses which are suitable for showing on the public web profile of an account.
AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
// AccountFollowersGet fetches a list of the target account's followers.
AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// AccountFollowingGet fetches a list of the accounts that target account is following.
AccountFollowingGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
AccountRelationshipGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AccountFollowCreate handles a follow request to an account, either remote or local.
AccountFollowCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
AccountFollowRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AccountBlockCreate handles the creation of a block from authed account to target account, either remote or local.
AccountBlockCreate(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AccountBlockRemove handles the removal of a block from authed account to target account, either remote or local.
AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AdminAccountAction handles the creation/execution of an action on an account.
AdminAccountAction(ctx context.Context, authed *oauth.Auth, form *apimodel.AdminAccountActionRequest) gtserror.WithCode
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode)
// AdminEmojisGet allows admins to view emojis based on various filters.
AdminEmojisGet(ctx context.Context, authed *oauth.Auth, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// AdminEmojiGet returns the admin view of an emoji with the given ID
AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
// AdminEmojiDelete deletes one *local* emoji with the given key. Remote emojis will not be deleted this way.
// Only admin users in good standing should be allowed to access this function -- check this before calling it.
AdminEmojiDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
// AdminEmojiUpdate updates one local or remote emoji with the given key.
// Only admin users in good standing should be allowed to access this function -- check this before calling it.
AdminEmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode)
// AdminEmojiCategoriesGet gets a list of all existing emoji categories.
AdminEmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode)
// AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form.
AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlocksImport handles the import of multiple domain blocks by an admin, using the given form.
AdminDomainBlocksImport(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) ([]*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlocksGet returns a list of currently blocked domains.
AdminDomainBlocksGet(ctx context.Context, authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlockGet returns one domain block, specified by ID.
AdminDomainBlockGet(ctx context.Context, authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlockDelete deletes one domain block, specified by ID, returning the deleted domain block.
AdminDomainBlockDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode)
// AdminMediaRemotePrune triggers a prune of remote media according to the given number of mediaRemoteCacheDays
AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
// AdminMediaRefetch triggers a refetch of remote media for the given domain (or all if domain is empty).
AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode
// AdminReportsGet returns a list of user moderation reports.
AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// AdminReportGet returns a single user moderation report, specified by id.
AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode)
// AdminReportResolve marks a single user moderation report as resolved, with the given id.
// actionTakenComment is optional: if set, this will be stored as a comment on the action taken.
AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode)
// AppCreate processes the creation of a new API application
AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode)
// BlocksGet returns a list of accounts blocked by the requesting account.
BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode)
// CustomEmojisGet returns an array of info about the custom emojis on this server
CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode)
// BookmarksGet returns a pageable response of statuses that have been bookmarked
BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// FileGet handles the fetching of a media attachment file via the fileserver.
FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode)
// FollowRequestsGet handles the getting of the authed account's incoming follow requests
FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode)
// FollowRequestAccept handles the acceptance of a follow request from the given account ID.
FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode)
// FollowRequestReject handles the rejection of a follow request from the given account ID.
FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode)
// InstanceGetV1 retrieves instance information for serving at api/v1/instance
InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode)
// InstanceGetV1 retrieves instance information for serving at api/v2/instance
InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode)
InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode)
// InstancePatch updates this instance according to the given form.
//
// It should already be ascertained that the requesting account is authenticated and an admin.
InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode)
// MediaCreate handles the creation of a media attachment, using the given form.
MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode)
// MediaGet handles the GET of a media attachment with the given ID
MediaGet(ctx context.Context, authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode)
// MediaUpdate handles the PUT of a media attachment with the given ID and form
MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
// NotificationsGet
NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode)
// NotificationsClear
NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode
OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode)
OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode
OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error)
// SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode)
// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
// StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through.
StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusUnboost processes the unboost/unreblog of a given status, returning the status if all is well.
StatusUnboost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// StatusGet gets the given status, taking account of privacy settings and blocks etc.
StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusGetContext returns the context (previous and following posts) from the given status ID
StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
// StatusBookmark process a bookmark for a status
StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusUnbookmark removes a bookmark for a status
StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
// PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters.
PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
// FavedTimelineGet returns faved statuses, with the given filters/parameters.
FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid.
AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode)
// OpenStreamForAccount opens a new stream for the given account, with the given stream type.
OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)
// UserChangePassword changes the password for the given user, with the given form.
UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode
// UserConfirmEmail confirms an email address using the given token.
// The user belonging to the confirmed email is also returned.
UserConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode)
// ReportsGet returns reports created by the given user.
ReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
// ReportGet returns one report created by the given user.
ReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.Report, gtserror.WithCode)
// ReportCreate creates a new report using the given account and form.
ReportCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode)
/*
FEDERATION API-FACING PROCESSING FUNCTIONS
These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
response, pass work to the processor using a channel instead.
*/
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
// before returning a JSON serializable interface to the caller.
GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediOutbox returns the public outbox of the requested user, with the given parameters.
GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediEmoji returns the AP representation of an emoji on this instance.
GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetNodeInfoRel returns a well known response giving the path to node info.
GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetNodeInfo returns a node info struct in response to a node info request.
GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode)
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
//
// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox.
// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
//
// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
//
// If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
//
// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}
// processor just implements the Processor interface
type processor struct {
type Processor struct {
clientWorker *concurrency.WorkerPool[messages.FromClientAPI]
fedWorker *concurrency.WorkerPool[messages.FromFederator]
federator federation.Federator
tc typeutils.TypeConverter
oauthServer oauth.Server
mediaManager media.Manager
mediaManager mm.Manager
storage *storage.Driver
statusTimelines timeline.Manager
db db.DB
@@ -313,14 +57,46 @@ type processor struct {
SUB-PROCESSORS
*/
accountProcessor account.Processor
adminProcessor admin.Processor
statusProcessor status.Processor
streamingProcessor streaming.Processor
mediaProcessor mediaProcessor.Processor
userProcessor user.Processor
federationProcessor federationProcessor.Processor
reportProcessor report.Processor
account account.Processor
admin admin.Processor
fedi fedi.Processor
media media.Processor
report report.Processor
status status.Processor
stream stream.Processor
user user.Processor
}
func (p *Processor) Account() *account.Processor {
return &p.account
}
func (p *Processor) Admin() *admin.Processor {
return &p.admin
}
func (p *Processor) Fedi() *fedi.Processor {
return &p.fedi
}
func (p *Processor) Media() *media.Processor {
return &p.media
}
func (p *Processor) Report() *report.Processor {
return &p.report
}
func (p *Processor) Status() *status.Processor {
return &p.status
}
func (p *Processor) Stream() *stream.Processor {
return &p.stream
}
func (p *Processor) User() *user.Processor {
return &p.user
}
// NewProcessor returns a new Processor.
@@ -328,26 +104,18 @@ func NewProcessor(
tc typeutils.TypeConverter,
federator federation.Federator,
oauthServer oauth.Server,
mediaManager media.Manager,
mediaManager mm.Manager,
storage *storage.Driver,
db db.DB,
emailSender email.Sender,
clientWorker *concurrency.WorkerPool[messages.FromClientAPI],
fedWorker *concurrency.WorkerPool[messages.FromFederator],
) Processor {
) *Processor {
parseMentionFunc := GetParseMentionFunc(db, federator)
statusProcessor := status.New(db, tc, clientWorker, parseMentionFunc)
streamingProcessor := streaming.New(db, oauthServer)
accountProcessor := account.New(db, tc, mediaManager, oauthServer, clientWorker, federator, parseMentionFunc)
adminProcessor := admin.New(db, tc, mediaManager, federator.TransportController(), storage, clientWorker)
mediaProcessor := mediaProcessor.New(db, tc, mediaManager, federator.TransportController(), storage)
userProcessor := user.New(db, emailSender)
federationProcessor := federationProcessor.New(db, tc, federator)
reportProcessor := report.New(db, tc, clientWorker)
filter := visibility.NewFilter(db)
return &processor{
return &Processor{
clientWorker: clientWorker,
fedWorker: fedWorker,
@@ -358,21 +126,22 @@ func NewProcessor(
storage: storage,
statusTimelines: timeline.NewManager(StatusGrabFunction(db), StatusFilterFunction(db, filter), StatusPrepareFunction(db, tc), StatusSkipInsertFunction()),
db: db,
filter: visibility.NewFilter(db),
filter: filter,
accountProcessor: accountProcessor,
adminProcessor: adminProcessor,
statusProcessor: statusProcessor,
streamingProcessor: streamingProcessor,
mediaProcessor: mediaProcessor,
userProcessor: userProcessor,
federationProcessor: federationProcessor,
reportProcessor: reportProcessor,
// sub processors
account: account.New(db, tc, mediaManager, oauthServer, clientWorker, federator, parseMentionFunc),
admin: admin.New(db, tc, mediaManager, federator.TransportController(), storage, clientWorker),
fedi: fedi.New(db, tc, federator),
media: media.New(db, tc, mediaManager, federator.TransportController(), storage),
report: report.New(db, tc, clientWorker),
status: status.New(db, tc, clientWorker, parseMentionFunc),
stream: stream.New(db, oauthServer),
user: user.New(db, emailSender),
}
}
// Start starts the Processor, reading from its channels and passing messages back and forth.
func (p *processor) Start() error {
func (p *Processor) Start() error {
// Setup and start the client API worker pool
p.clientWorker.SetProcessor(p.ProcessFromClientAPI)
if err := p.clientWorker.Start(); err != nil {
@@ -394,7 +163,7 @@ func (p *processor) Start() error {
}
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
func (p *processor) Stop() error {
func (p *Processor) Stop() error {
if err := p.clientWorker.Stop(); err != nil {
return err
}

View File

@@ -62,7 +62,7 @@ type ProcessingStandardTestSuite struct {
testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
processor processing.Processor
processor *processing.Processor
}
func (suite *ProcessingStandardTestSuite) SetupSuite() {

View File

@@ -1,39 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) ReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
return p.reportProcessor.ReportsGet(ctx, authed.Account, resolved, targetAccountID, maxID, sinceID, minID, limit)
}
func (p *processor) ReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.Report, gtserror.WithCode) {
return p.reportProcessor.ReportGet(ctx, authed.Account, id)
}
func (p *processor) ReportCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) {
return p.reportProcessor.Create(ctx, authed.Account, form)
}

View File

@@ -33,7 +33,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) {
// Create creates one user report / flag, using the provided form parameters.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) {
if account.ID == form.AccountID {
err := errors.New("cannot report your own account")
return nil, gtserror.NewErrorBadRequest(err, err.Error())

View File

@@ -30,7 +30,40 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
// Get returns the user view of a moderation report, with the given id.
func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
if report.AccountID != account.ID {
err = fmt.Errorf("report with id %s does not belong to account %s", report.ID, account.ID)
return nil, gtserror.NewErrorNotFound(err)
}
apiReport, err := p.tc.ReportToAPIReport(ctx, report)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err))
}
return apiReport, nil
}
// GetMultiple returns multiple reports created by the given account, filtered according to the provided parameters.
func (p *Processor) GetMultiple(
ctx context.Context,
account *gtsmodel.Account,
resolved *bool,
targetAccountID string,
maxID string,
sinceID string,
minID string,
limit int,
) (*apimodel.PageableResponse, gtserror.WithCode) {
reports, err := p.db.GetReports(ctx, resolved, account.ID, targetAccountID, maxID, sinceID, minID, limit)
if err != nil {
if err == db.ErrNoEntries {

View File

@@ -1,51 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 report
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode) {
report, err := p.db.GetReportByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
if report.AccountID != account.ID {
err = fmt.Errorf("report with id %s does not belong to account %s", report.ID, account.ID)
return nil, gtserror.NewErrorNotFound(err)
}
apiReport, err := p.tc.ReportToAPIReport(ctx, report)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err))
}
return apiReport, nil
}

View File

@@ -19,31 +19,20 @@
package report
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type Processor interface {
ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode)
Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode)
}
type processor struct {
type Processor struct {
db db.DB
tc typeutils.TypeConverter
clientWorker *concurrency.WorkerPool[messages.FromClientAPI]
}
func New(db db.DB, tc typeutils.TypeConverter, clientWorker *concurrency.WorkerPool[messages.FromClientAPI]) Processor {
return &processor{
return Processor{
tc: tc,
db: db,
clientWorker: clientWorker,

View File

@@ -49,7 +49,7 @@ import (
// The only exception to this is when we get a malformed query, in
// which case we return a bad request error so the user knows they
// did something funky.
func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) {
func (p *Processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) {
// tidy up the query and make sure it wasn't just spaces
query := strings.TrimSpace(search.Query)
if query == "" {
@@ -223,7 +223,7 @@ func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *a
return searchResult, nil
}
func (p *processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL) (*gtsmodel.Status, error) {
func (p *Processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL) (*gtsmodel.Status, error) {
status, statusable, err := p.federator.GetStatus(transport.WithFastfail(ctx), authed.Account.Username, uri, true, true)
if err != nil {
return nil, err
@@ -237,7 +237,7 @@ func (p *processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, u
return status, nil
}
func (p *processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) {
func (p *Processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) {
if !resolve {
var (
account *gtsmodel.Account
@@ -272,7 +272,7 @@ func (p *processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth,
)
}
func (p *processor) searchAccountByUsernameDomain(ctx context.Context, authed *oauth.Auth, username string, domain string, resolve bool) (*gtsmodel.Account, error) {
func (p *Processor) searchAccountByUsernameDomain(ctx context.Context, authed *oauth.Auth, username string, domain string, resolve bool) (*gtsmodel.Account, error) {
if !resolve {
if domain == config.GetHost() || domain == config.GetAccountDomain() {
// We do local lookups using an empty domain,

View File

@@ -1,75 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 processing
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Create(ctx, authed.Account, authed.Application, form)
}
func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Delete(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Fave(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Boost(ctx, authed.Account, authed.Application, targetStatusID)
}
func (p *processor) StatusUnboost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Unboost(ctx, authed.Account, authed.Application, targetStatusID)
}
func (p *processor) StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
return p.statusProcessor.BoostedBy(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
return p.statusProcessor.FavedBy(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Get(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Unfave(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
return p.statusProcessor.Context(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Bookmark(ctx, authed.Account, targetStatusID)
}
func (p *processor) StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Unbookmark(ctx, authed.Account, targetStatusID)
}

View File

@@ -30,7 +30,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
)
func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -79,3 +80,43 @@ func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Ac
return apiStatus, nil
}
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// first check if the status is actually bookmarked
toUnbookmark := false
gtsBookmark := &gtsmodel.StatusBookmark{}
if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
// we have a bookmark for this status
toUnbookmark = true
}
if toUnbookmark {
if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
}
// return the api representation of the target status
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}

View File

@@ -36,13 +36,33 @@ func (suite *StatusBookmarkTestSuite) TestBookmark() {
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark1)
suite.True(bookmark1.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
}
func (suite *StatusBookmarkTestSuite) TestUnbookmark() {
ctx := context.Background()
// bookmark a status
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark1)
suite.True(bookmark1.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
bookmark2, err := suite.status.BookmarkRemove(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark2)
suite.False(bookmark2.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
}
func TestStatusBookmarkTestSuite(t *testing.T) {
suite.Run(t, new(StatusBookmarkTestSuite))
}

View File

@@ -25,12 +25,14 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
// BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well.
func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -93,3 +95,153 @@ func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Accou
return apiStatus, nil
}
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// check if we actually have a boost for this status
var toUnboost bool
gtsBoost := &gtsmodel.Status{}
where := []db.Where{
{
Key: "boost_of_id",
Value: targetStatusID,
},
{
Key: "account_id",
Value: requestingAccount.ID,
},
}
err = p.db.GetWhere(ctx, where, gtsBoost)
if err == nil {
// we have a boost
toUnboost = true
}
if err != nil {
// something went wrong in the db finding the boost
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
}
// we just don't have a boost
toUnboost = false
}
if toUnboost {
// pin some stuff onto the boost while we have it out of the db
gtsBoost.Account = requestingAccount
gtsBoost.BoostOf = targetStatus
gtsBoost.BoostOfAccount = targetStatus.Account
gtsBoost.BoostOf.Account = targetStatus.Account
// send it back to the processor for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityUndo,
GTSModel: gtsBoost,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
if boostOfID := targetStatus.BoostOfID; boostOfID != "" {
// the target status is a boost wrapper, redirect this request to the status it boosts
boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
targetStatus = boostedStatus
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
return nil, gtserror.NewErrorNotFound(err)
}
if !visible {
err = errors.New("BoostedBy: status is not visible")
return nil, gtserror.NewErrorNotFound(err)
}
statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus)
if err != nil {
err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err)
return nil, gtserror.NewErrorNotFound(err)
}
// filter account IDs so the user doesn't see accounts they blocked or which blocked them
accountIDs := make([]string, 0, len(statusReblogs))
for _, s := range statusReblogs {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true)
if err != nil {
err = fmt.Errorf("BoostedBy: error checking blocks: %s", err)
return nil, gtserror.NewErrorNotFound(err)
}
if !blocked {
accountIDs = append(accountIDs, s.AccountID)
}
}
// TODO: filter other things here? suspended? muted? silenced?
// fetch accounts + create their API representations
apiAccounts := make([]*apimodel.Account, 0, len(accountIDs))
for _, accountID := range accountIDs {
account, err := p.db.GetAccountByID(ctx, accountID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account)
if err != nil {
err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiAccounts = append(apiAccounts, apiAccount)
}
return apiAccounts, nil
}

View File

@@ -37,7 +37,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() {
application1 := suite.testApplications["application_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
boost1, err := suite.status.Boost(ctx, boostingAccount1, application1, targetStatus1.ID)
boost1, err := suite.status.BoostCreate(ctx, boostingAccount1, application1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(boost1)
suite.Equal(targetStatus1.ID, boost1.Reblog.ID)
@@ -47,7 +47,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() {
application2 := suite.testApplications["application_2"]
targetStatus2ID := boost1.ID
boost2, err := suite.status.Boost(ctx, boostingAccount2, application2, targetStatus2ID)
boost2, err := suite.status.BoostCreate(ctx, boostingAccount2, application2, targetStatus2ID)
suite.NoError(err)
suite.NotNil(boost2)
// the boosted status should not be the boost,

View File

@@ -1,107 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) BoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
if boostOfID := targetStatus.BoostOfID; boostOfID != "" {
// the target status is a boost wrapper, redirect this request to the status it boosts
boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
targetStatus = boostedStatus
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
return nil, gtserror.NewErrorNotFound(err)
}
if !visible {
err = errors.New("BoostedBy: status is not visible")
return nil, gtserror.NewErrorNotFound(err)
}
statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus)
if err != nil {
err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err)
return nil, gtserror.NewErrorNotFound(err)
}
// filter account IDs so the user doesn't see accounts they blocked or which blocked them
accountIDs := make([]string, 0, len(statusReblogs))
for _, s := range statusReblogs {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true)
if err != nil {
err = fmt.Errorf("BoostedBy: error checking blocks: %s", err)
return nil, gtserror.NewErrorNotFound(err)
}
if !blocked {
accountIDs = append(accountIDs, s.AccountID)
}
}
// TODO: filter other things here? suspended? muted? silenced?
// fetch accounts + create their API representations
apiAccounts := make([]*apimodel.Account, 0, len(accountIDs))
for _, accountID := range accountIDs {
account, err := p.db.GetAccountByID(ctx, accountID)
if err != nil {
wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err)
if !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(wrapped)
}
return nil, gtserror.NewErrorNotFound(wrapped)
}
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account)
if err != nil {
err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
apiAccounts = append(apiAccounts, apiAccount)
}
return apiAccounts, nil
}

View File

@@ -1,87 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
"sort"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Context(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
context := &apimodel.Context{
Ancestors: []apimodel.Status{},
Descendants: []apimodel.Status{},
}
parents, err := p.db.GetStatusParents(ctx, targetStatus, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
for _, status := range parents {
if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
if err == nil {
context.Ancestors = append(context.Ancestors, *apiStatus)
}
}
}
sort.Slice(context.Ancestors, func(i int, j int) bool {
return context.Ancestors[i].ID < context.Ancestors[j].ID
})
children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "")
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
for _, status := range children {
if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
if err == nil {
context.Descendants = append(context.Descendants, *apiStatus)
}
}
}
return context, nil
}

View File

@@ -20,20 +20,25 @@ package status
import (
"context"
"errors"
"fmt"
"time"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
accountURIs := uris.GenerateURIsForAccount(account.Username)
thisStatusID := id.NewULID()
local := true
@@ -56,23 +61,23 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli
Text: form.Status,
}
if errWithCode := p.ProcessReplyToID(ctx, form, account.ID, newStatus); errWithCode != nil {
if errWithCode := processReplyToID(ctx, p.db, form, account.ID, newStatus); errWithCode != nil {
return nil, errWithCode
}
if errWithCode := p.ProcessMediaIDs(ctx, form, account.ID, newStatus); errWithCode != nil {
if errWithCode := processMediaIDs(ctx, p.db, form, account.ID, newStatus); errWithCode != nil {
return nil, errWithCode
}
if err := p.ProcessVisibility(ctx, form, account.Privacy, newStatus); err != nil {
if err := processVisibility(ctx, form, account.Privacy, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if err := p.ProcessLanguage(ctx, form, account.Language, newStatus); err != nil {
if err := processLanguage(ctx, form, account.Language, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if err := p.ProcessContent(ctx, form, account.ID, newStatus); err != nil {
if err := processContent(ctx, p.db, p.formatter, p.parseMention, form, account.ID, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -97,3 +102,249 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli
return apiStatus, nil
}
func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.InReplyToID == "" {
return nil
}
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
//
// 1. Does the replied status exist in the database?
// 2. Is the replied status marked as replyable?
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
//
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
repliedStatus := &gtsmodel.Status{}
repliedAccount := &gtsmodel.Account{}
if err := dbService.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
}
if !*repliedStatus.Replyable {
err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
return gtserror.NewErrorForbidden(err, err.Error())
}
if err := dbService.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
return gtserror.NewErrorInternalError(err)
}
if blocked, err := dbService.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil {
err := fmt.Errorf("db error checking block: %s", err)
return gtserror.NewErrorInternalError(err)
} else if blocked {
err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
return gtserror.NewErrorNotFound(err)
}
status.InReplyToID = repliedStatus.ID
status.InReplyToURI = repliedStatus.URI
status.InReplyToAccountID = repliedAccount.ID
return nil
}
func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.MediaIDs == nil {
return nil
}
attachments := []*gtsmodel.MediaAttachment{}
attachmentIDs := []string{}
for _, mediaID := range form.MediaIDs {
attachment, err := dbService.GetAttachmentByID(ctx, mediaID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
return gtserror.NewErrorInternalError(err)
}
if attachment.AccountID != thisAccountID {
err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
minDescriptionChars := config.GetMediaDescriptionMinChars()
if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
attachments = append(attachments, attachment)
attachmentIDs = append(attachmentIDs, attachment.ID)
}
status.Attachments = attachments
status.AttachmentIDs = attachmentIDs
return nil
}
func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
// by default all flags are set to true
federated := true
boostable := true
replyable := true
likeable := true
// If visibility isn't set on the form, then just take the account default.
// If that's also not set, take the default for the whole instance.
var vis gtsmodel.Visibility
switch {
case form.Visibility != "":
vis = typeutils.APIVisToVis(form.Visibility)
case accountDefaultVis != "":
vis = accountDefaultVis
default:
vis = gtsmodel.VisibilityDefault
}
switch vis {
case gtsmodel.VisibilityPublic:
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
break
case gtsmodel.VisibilityUnlocked:
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
if form.Federated != nil {
federated = *form.Federated
}
if form.Boostable != nil {
boostable = *form.Boostable
}
if form.Replyable != nil {
replyable = *form.Replyable
}
if form.Likeable != nil {
likeable = *form.Likeable
}
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
boostable = false
if form.Federated != nil {
federated = *form.Federated
}
if form.Replyable != nil {
replyable = *form.Replyable
}
if form.Likeable != nil {
likeable = *form.Likeable
}
case gtsmodel.VisibilityDirect:
// direct is pretty easy: there's only one possible setting so return it
federated = true
boostable = false
replyable = true
likeable = true
}
status.Visibility = vis
status.Federated = &federated
status.Boostable = &boostable
status.Replyable = &replyable
status.Likeable = &likeable
return nil
}
func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
if form.Language != "" {
status.Language = form.Language
} else {
status.Language = accountDefaultLanguage
}
if status.Language == "" {
return errors.New("no language given either in status create form or account default")
}
return nil
}
func processContent(ctx context.Context, dbService db.DB, formatter text.Formatter, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
// if there's nothing in the status at all we can just return early
if form.Status == "" {
status.Content = ""
return nil
}
// if format wasn't specified we should try to figure out what format this user prefers
if form.Format == "" {
acct, err := dbService.GetAccountByID(ctx, accountID)
if err != nil {
return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
}
switch acct.StatusFormat {
case "plain":
form.Format = apimodel.StatusFormatPlain
case "markdown":
form.Format = apimodel.StatusFormatMarkdown
default:
form.Format = apimodel.StatusFormatDefault
}
}
// parse content out of the status depending on what format has been submitted
var f text.FormatFunc
switch form.Format {
case apimodel.StatusFormatPlain:
f = formatter.FromPlain
case apimodel.StatusFormatMarkdown:
f = formatter.FromMarkdown
default:
return fmt.Errorf("format %s not recognised as a valid status format", form.Format)
}
formatted := f(ctx, parseMention, accountID, status.ID, form.Status)
// add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
// add just their ids to the status for putting in the db
status.Mentions = formatted.Mentions
status.MentionIDs = make([]string, 0, len(formatted.Mentions))
for _, gtsmention := range formatted.Mentions {
status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
}
status.Tags = formatted.Tags
status.TagIDs = make([]string, 0, len(formatted.Tags))
for _, gtstag := range formatted.Tags {
status.TagIDs = append(status.TagIDs, gtstag.ID)
}
status.Emojis = formatted.Emojis
status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
for _, gtsemoji := range formatted.Emojis {
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText)
for _, gtsemoji := range spoilerformatted.Emojis {
status.Emojis = append(status.Emojis, gtsemoji)
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
status.Content = formatted.HTML
return nil
}

View File

@@ -30,7 +30,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
// Delete processes the delete of a given status, returning the deleted status if the delete goes through.
func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))

View File

@@ -33,7 +33,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
// FaveCreate processes the faving of a given status, returning the updated status if the fave goes through.
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -98,3 +99,111 @@ func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Accoun
return apiStatus, nil
}
// FaveRemove processes the unfaving of a given status, returning the updated status if the fave goes through.
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// check if we actually have a fave for this status
var toUnfave bool
gtsFave := &gtsmodel.StatusFave{}
err = p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
if err == nil {
// we have a fave
toUnfave = true
}
if err != nil {
// something went wrong in the db finding the fave
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
}
// we just don't have a fave
toUnfave = false
}
if toUnfave {
// we had a fave, so take some action to get rid of it
if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
// send it back to the processor for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityUndo,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
statusFaves, err := p.db.GetStatusFaves(ctx, targetStatus)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
}
// filter the list so the user doesn't see accounts they blocked or which blocked them
filteredAccounts := []*gtsmodel.Account{}
for _, fave := range statusFaves {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
}
if !blocked {
filteredAccounts = append(filteredAccounts, fave.Account)
}
}
// now we can return the api representation of those accounts
apiAccounts := []*apimodel.Account{}
for _, acc := range filteredAccounts {
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
apiAccounts = append(apiAccounts, apiAccount)
}
return apiAccounts, nil
}

View File

@@ -1,76 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
statusFaves, err := p.db.GetStatusFaves(ctx, targetStatus)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
}
// filter the list so the user doesn't see accounts they blocked or which blocked them
filteredAccounts := []*gtsmodel.Account{}
for _, fave := range statusFaves {
blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
}
if !blocked {
filteredAccounts = append(filteredAccounts, fave.Account)
}
}
// now we can return the api representation of those accounts
apiAccounts := []*apimodel.Account{}
for _, acc := range filteredAccounts {
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
apiAccounts = append(apiAccounts, apiAccount)
}
return apiAccounts, nil
}

View File

@@ -22,13 +22,15 @@ import (
"context"
"errors"
"fmt"
"sort"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
// Get gets the given status, taking account of privacy settings and blocks etc.
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -52,3 +54,61 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
return apiStatus, nil
}
// ContextGet returns the context (previous and following posts) from the given status ID.
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
context := &apimodel.Context{
Ancestors: []apimodel.Status{},
Descendants: []apimodel.Status{},
}
parents, err := p.db.GetStatusParents(ctx, targetStatus, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
for _, status := range parents {
if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
if err == nil {
context.Ancestors = append(context.Ancestors, *apiStatus)
}
}
}
sort.Slice(context.Ancestors, func(i int, j int) bool {
return context.Ancestors[i].ID < context.Ancestors[j].ID
})
children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "")
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
for _, status := range children {
if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
if err == nil {
context.Descendants = append(context.Descendants, *apiStatus)
}
}
}
return context, nil
}

View File

@@ -19,12 +19,8 @@
package status
import (
"context"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
@@ -32,45 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
// Processor wraps a bunch of functions for processing statuses.
type Processor interface {
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
// Delete processes the delete of a given status, returning the deleted status if the delete goes through.
Delete(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Fave processes the faving of a given status, returning the updated status if the fave goes through.
Fave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
Boost(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
Unboost(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
BoostedBy(ctx context.Context, account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
FavedBy(ctx context.Context, account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// Get gets the given status, taking account of privacy settings and blocks etc.
Get(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Unfave processes the unfaving of a given status, returning the updated status if the fave goes through.
Unfave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Context returns the context (previous and following posts) from the given status ID
Context(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
// Bookmarks a status
Bookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// Removes a bookmark for a status
Unbookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
/*
PROCESSING UTILS
*/
ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error
ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode
ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode
ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error
ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error
}
type processor struct {
type Processor struct {
tc typeutils.TypeConverter
db db.DB
filter visibility.Filter
@@ -81,7 +39,7 @@ type processor struct {
// New returns a new status processor.
func New(db db.DB, tc typeutils.TypeConverter, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], parseMention gtsmodel.ParseMentionFunc) Processor {
return &processor{
return Processor{
tc: tc,
db: db,
filter: visibility.NewFilter(db),

View File

@@ -1,69 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Unbookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// first check if the status is already bookmarked
toUnbookmark := false
gtsBookmark := &gtsmodel.StatusBookmark{}
if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
// we already have a bookmark for this status
toUnbookmark = true
}
if toUnbookmark {
if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
}
// return the apidon representation of the target status
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}

View File

@@ -1,54 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
)
type StatusUnbookmarkTestSuite struct {
StatusStandardTestSuite
}
func (suite *StatusUnbookmarkTestSuite) TestUnbookmark() {
ctx := context.Background()
// bookmark a status
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark1)
suite.True(bookmark1.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
bookmark2, err := suite.status.Unbookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark2)
suite.False(bookmark2.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
}
func TestStatusUnbookmarkTestSuite(t *testing.T) {
suite.Run(t, new(StatusUnbookmarkTestSuite))
}

View File

@@ -1,103 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// check if we actually have a boost for this status
var toUnboost bool
gtsBoost := &gtsmodel.Status{}
where := []db.Where{
{
Key: "boost_of_id",
Value: targetStatusID,
},
{
Key: "account_id",
Value: requestingAccount.ID,
},
}
err = p.db.GetWhere(ctx, where, gtsBoost)
if err == nil {
// we have a boost
toUnboost = true
}
if err != nil {
// something went wrong in the db finding the boost
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
}
// we just don't have a boost
toUnboost = false
}
if toUnboost {
// pin some stuff onto the boost while we have it out of the db
gtsBoost.Account = requestingAccount
gtsBoost.BoostOf = targetStatus
gtsBoost.BoostOfAccount = targetStatus.Account
gtsBoost.BoostOf.Account = targetStatus.Account
// send it back to the processor for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityUndo,
GTSModel: gtsBoost,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}

View File

@@ -1,91 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (p *processor) Unfave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
// check if we actually have a fave for this status
var toUnfave bool
gtsFave := &gtsmodel.StatusFave{}
err = p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
if err == nil {
// we have a fave
toUnfave = true
}
if err != nil {
// something went wrong in the db finding the fave
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
}
// we just don't have a fave
toUnfave = false
}
if toUnfave {
// we had a fave, so take some action to get rid of it
if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
// send it back to the processor for async processing
p.clientWorker.Queue(messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityUndo,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
}

View File

@@ -1,278 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
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 status
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/text"
)
func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
// by default all flags are set to true
federated := true
boostable := true
replyable := true
likeable := true
// If visibility isn't set on the form, then just take the account default.
// If that's also not set, take the default for the whole instance.
var vis gtsmodel.Visibility
switch {
case form.Visibility != "":
vis = p.tc.APIVisToVis(form.Visibility)
case accountDefaultVis != "":
vis = accountDefaultVis
default:
vis = gtsmodel.VisibilityDefault
}
switch vis {
case gtsmodel.VisibilityPublic:
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
break
case gtsmodel.VisibilityUnlocked:
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
if form.Federated != nil {
federated = *form.Federated
}
if form.Boostable != nil {
boostable = *form.Boostable
}
if form.Replyable != nil {
replyable = *form.Replyable
}
if form.Likeable != nil {
likeable = *form.Likeable
}
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
boostable = false
if form.Federated != nil {
federated = *form.Federated
}
if form.Replyable != nil {
replyable = *form.Replyable
}
if form.Likeable != nil {
likeable = *form.Likeable
}
case gtsmodel.VisibilityDirect:
// direct is pretty easy: there's only one possible setting so return it
federated = true
boostable = false
replyable = true
likeable = true
}
status.Visibility = vis
status.Federated = &federated
status.Boostable = &boostable
status.Replyable = &replyable
status.Likeable = &likeable
return nil
}
func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.InReplyToID == "" {
return nil
}
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
//
// 1. Does the replied status exist in the database?
// 2. Is the replied status marked as replyable?
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
//
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
repliedStatus := &gtsmodel.Status{}
repliedAccount := &gtsmodel.Account{}
if err := p.db.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
}
if !*repliedStatus.Replyable {
err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
return gtserror.NewErrorForbidden(err, err.Error())
}
if err := p.db.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
return gtserror.NewErrorInternalError(err)
}
if blocked, err := p.db.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil {
err := fmt.Errorf("db error checking block: %s", err)
return gtserror.NewErrorInternalError(err)
} else if blocked {
err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
return gtserror.NewErrorNotFound(err)
}
status.InReplyToID = repliedStatus.ID
status.InReplyToURI = repliedStatus.URI
status.InReplyToAccountID = repliedAccount.ID
return nil
}
func (p *processor) ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.MediaIDs == nil {
return nil
}
attachments := []*gtsmodel.MediaAttachment{}
attachmentIDs := []string{}
for _, mediaID := range form.MediaIDs {
attachment, err := p.db.GetAttachmentByID(ctx, mediaID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
return gtserror.NewErrorInternalError(err)
}
if attachment.AccountID != thisAccountID {
err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
minDescriptionChars := config.GetMediaDescriptionMinChars()
if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
attachments = append(attachments, attachment)
attachmentIDs = append(attachmentIDs, attachment.ID)
}
status.Attachments = attachments
status.AttachmentIDs = attachmentIDs
return nil
}
func (p *processor) ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
if form.Language != "" {
status.Language = form.Language
} else {
status.Language = accountDefaultLanguage
}
if status.Language == "" {
return errors.New("no language given either in status create form or account default")
}
return nil
}
func (p *processor) ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
// if there's nothing in the status at all we can just return early
if form.Status == "" {
status.Content = ""
return nil
}
// if format wasn't specified we should try to figure out what format this user prefers
if form.Format == "" {
acct, err := p.db.GetAccountByID(ctx, accountID)
if err != nil {
return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
}
switch acct.StatusFormat {
case "plain":
form.Format = apimodel.StatusFormatPlain
case "markdown":
form.Format = apimodel.StatusFormatMarkdown
default:
form.Format = apimodel.StatusFormatDefault
}
}
// parse content out of the status depending on what format has been submitted
var f text.FormatFunc
switch form.Format {
case apimodel.StatusFormatPlain:
f = p.formatter.FromPlain
case apimodel.StatusFormatMarkdown:
f = p.formatter.FromMarkdown
default:
return fmt.Errorf("format %s not recognised as a valid status format", form.Format)
}
formatted := f(ctx, p.parseMention, accountID, status.ID, form.Status)
// add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
// add just their ids to the status for putting in the db
status.Mentions = formatted.Mentions
status.MentionIDs = make([]string, 0, len(formatted.Mentions))
for _, gtsmention := range formatted.Mentions {
status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
}
status.Tags = formatted.Tags
status.TagIDs = make([]string, 0, len(formatted.Tags))
for _, gtstag := range formatted.Tags {
status.TagIDs = append(status.TagIDs, gtstag.ID)
}
status.Emojis = formatted.Emojis
status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
for _, gtsemoji := range formatted.Emojis {
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
spoilerformatted := p.formatter.FromPlainEmojiOnly(ctx, p.parseMention, accountID, status.ID, form.SpoilerText)
for _, gtsemoji := range spoilerformatted.Emojis {
status.Emojis = append(status.Emojis, gtsemoji)
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
status.Content = formatted.HTML
return nil
}

Some files were not shown because too many files have changed in this diff Show More