Domain block (#76)

* start work on admin domain blocking

* move stuff around + further work on domain blocks

* move + restructure processor

* prep work for deleting account

* tidy

* go fmt

* formatting

* domain blocking more work

* check domain blocks way earlier on

* progress on delete account

* delete more stuff when an account is gone

* and more...

* domain blocky block block

* get individual domain block, delete a block
This commit is contained in:
Tobi Smethurst 2021-07-05 13:23:03 +02:00 committed by GitHub
commit d389e7b150
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 3447 additions and 1419 deletions

View file

@ -19,512 +19,43 @@
package processing
import (
"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"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// accountCreate does the dirty work of making an account and user in the database.
// It then returns a token to the caller, for use with the new account, as per the
// spec here: https://docs.joinmastodon.org/methods/accounts/
func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
l := p.log.WithField("func", "accountCreate")
if err := p.db.IsEmailAvailable(form.Email); err != nil {
return nil, err
}
if err := p.db.IsUsernameAvailable(form.Username); err != nil {
return nil, err
}
// don't store a reason if we don't require one
reason := form.Reason
if !p.config.AccountsConfig.ReasonRequired {
reason = ""
}
l.Trace("creating new username and account")
user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID)
if err != nil {
return nil, fmt.Errorf("error creating new signup in the database: %s", err)
}
l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID)
accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
if err != nil {
return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
}
return &apimodel.Token{
AccessToken: accessToken.GetAccess(),
TokenType: "Bearer",
Scope: accessToken.GetScope(),
CreatedAt: accessToken.GetAccessCreateAt().Unix(),
}, nil
return p.accountProcessor.Create(authed.Token, authed.Application, form)
}
func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, errors.New("account not found")
}
return nil, fmt.Errorf("db error: %s", err)
}
// lazily dereference things on the account if it hasn't been done yet
var requestingUsername string
if authed.Account != nil {
requestingUsername = authed.Account.Username
}
if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
}
var mastoAccount *apimodel.Account
var err error
if authed.Account != nil && targetAccount.ID == authed.Account.ID {
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
} else {
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
}
if err != nil {
return nil, fmt.Errorf("error converting account: %s", err)
}
return mastoAccount, nil
return p.accountProcessor.Get(authed.Account, targetAccountID)
}
func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
l := p.log.WithField("func", "AccountUpdate")
if form.Discoverable != nil {
if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, &gtsmodel.Account{}); err != nil {
return nil, fmt.Errorf("error updating discoverable: %s", err)
}
}
if form.Bot != nil {
if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, &gtsmodel.Account{}); err != nil {
return nil, fmt.Errorf("error updating bot: %s", err)
}
}
if form.DisplayName != nil {
if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Note != nil {
if err := util.ValidateNote(*form.Note); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Avatar != nil && form.Avatar.Size != 0 {
avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
if err != nil {
return nil, err
}
l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
}
if form.Header != nil && form.Header.Size != 0 {
headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
if err != nil {
return nil, err
}
l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
}
if form.Locked != nil {
if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source != nil {
if form.Source.Language != nil {
if err := util.ValidateLanguage(*form.Source.Language); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source.Sensitive != nil {
if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source.Privacy != nil {
if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
}
// fetch the account with all updated values set
updatedAccount := &gtsmodel.Account{}
if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
}
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsUpdate,
GTSModel: updatedAccount,
OriginAccount: updatedAccount,
}
acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
if err != nil {
return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
}
return acctSensitive, nil
return p.accountProcessor.Update(authed.Account, form)
}
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
}
return nil, gtserror.NewErrorInternalError(err)
}
statuses := []gtsmodel.Status{}
apiStatuses := []apimodel.Status{}
if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit, excludeReplies, maxID, pinned, mediaOnly); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return apiStatuses, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, s := range statuses {
visible, err := p.filter.StatusVisible(&s, authed.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
}
if !visible {
continue
}
apiStatus, err := p.tc.StatusToMasto(&s, authed.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
}
apiStatuses = append(apiStatuses, *apiStatus)
}
return apiStatuses, nil
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
return p.accountProcessor.StatusesGet(authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
}
func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
followers := []gtsmodel.Follow{}
accounts := []apimodel.Account{}
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range followers {
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
a := &gtsmodel.Account{}
if err := p.db.GetByID(f.AccountID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
// derefence account fields in case we haven't done it already
if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
}
account, err := p.tc.AccountToMastoPublic(a)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
return p.accountProcessor.FollowersGet(authed.Account, targetAccountID)
}
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
following := []gtsmodel.Follow{}
accounts := []apimodel.Account{}
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range following {
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
a := &gtsmodel.Account{}
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
// derefence account fields in case we haven't done it already
if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
}
account, err := p.tc.AccountToMastoPublic(a)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
return p.accountProcessor.FollowingGet(authed.Account, targetAccountID)
}
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
if authed == nil || authed.Account == nil {
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
}
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
}
r, err := p.tc.RelationshipToMasto(gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
}
return r, nil
return p.accountProcessor.RelationshipGet(authed.Account, targetAccountID)
}
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't create the request ofc
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
}
// make sure the target account actually exists in our db
targetAcct := &gtsmodel.Account{}
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
}
}
// check if a follow exists already
follows, err := p.db.Follows(authed.Account, targetAcct)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
}
if follows {
// already follows so just return the relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// check if a follow exists already
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
}
if followRequested {
// already follow requested so just return the relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// make the follow request
newFollowID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
fr := &gtsmodel.FollowRequest{
ID: newFollowID,
AccountID: authed.Account.ID,
TargetAccountID: form.TargetAccountID,
ShowReblogs: true,
URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),
Notify: false,
}
if form.Reblogs != nil {
fr.ShowReblogs = *form.Reblogs
}
if form.Notify != nil {
fr.Notify = *form.Notify
}
// whack it in the database
if err := p.db.Put(fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
}
// if it's a local account that's not locked we can just straight up accept the follow request
if !targetAcct.Locked && targetAcct.Domain == "" {
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
}
// return the new relationship
return p.AccountRelationshipGet(authed, form.TargetAccountID)
}
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: fr,
OriginAccount: authed.Account,
TargetAccount: targetAcct,
}
// return whatever relationship results from this
return p.AccountRelationshipGet(authed, form.TargetAccountID)
return p.accountProcessor.FollowCreate(authed.Account, form)
}
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't do anything
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
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 := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
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([]db.Where{
{Key: "account_id", Value: authed.Account.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.db.DeleteByID(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([]db.Where{
{Key: "account_id", Value: authed.Account.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.db.DeleteByID(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.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: &gtsmodel.Follow{
AccountID: authed.Account.ID,
TargetAccountID: targetAccountID,
URI: frURI,
},
OriginAccount: authed.Account,
TargetAccount: targetAcct,
}
}
// follow status changed so send the UNDO activity to the channel for async processing
if fChanged {
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: &gtsmodel.Follow{
AccountID: authed.Account.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: authed.Account,
TargetAccount: targetAcct,
}
}
// return whatever relationship results from all this
return p.AccountRelationshipGet(authed, targetAccountID)
return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
}

View file

@ -0,0 +1,96 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"mime/multipart"
"github.com/sirupsen/logrus"
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/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"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(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
Delete(account *gtsmodel.Account, deletedBy string) error
// Get processes the given request for account information.
Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)
// Update processes the update of an account with the given form
Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers.
FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// FollowingGet fetches a list of the accounts that target account is following.
FollowingGet(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(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// FollowCreate handles a follow request to an account, either remote or local.
FollowCreate(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(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// 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.
UpdateAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
// 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.
UpdateHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
}
type processor struct {
tc typeutils.TypeConverter
config *config.Config
mediaHandler media.Handler
fromClientAPI chan gtsmodel.FromClientAPI
oauthServer oauth.Server
filter visibility.Filter
db db.DB
federator federation.Federator
log *logrus.Logger
}
// New returns a new account processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan gtsmodel.FromClientAPI, federator federation.Federator, config *config.Config, log *logrus.Logger) Processor {
return &processor{
tc: tc,
config: config,
mediaHandler: mediaHandler,
fromClientAPI: fromClientAPI,
oauthServer: oauthServer,
filter: visibility.NewFilter(db, log),
db: db,
federator: federator,
log: log,
}
}

View file

@ -0,0 +1,64 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/oauth2/v4"
)
func (p *processor) Create(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
l := p.log.WithField("func", "accountCreate")
if err := p.db.IsEmailAvailable(form.Email); err != nil {
return nil, err
}
if err := p.db.IsUsernameAvailable(form.Username); err != nil {
return nil, err
}
// don't store a reason if we don't require one
reason := form.Reason
if !p.config.AccountsConfig.ReasonRequired {
reason = ""
}
l.Trace("creating new username and account")
user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, application.ID)
if err != nil {
return nil, fmt.Errorf("error creating new signup in the database: %s", err)
}
l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID)
accessToken, err := p.oauthServer.GenerateUserAccessToken(applicationToken, application.ClientSecret, user.ID)
if err != nil {
return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
}
return &apimodel.Token{
AccessToken: accessToken.GetAccess(),
TokenType: "Bearer",
Scope: accessToken.GetScope(),
CreatedAt: accessToken.GetAccessCreateAt().Unix(),
}, nil
}

View file

@ -0,0 +1,116 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) FollowCreate(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
blocked, err := p.db.Blocked(requestingAccount.ID, form.TargetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
}
// make sure the target account actually exists in our db
targetAcct := &gtsmodel.Account{}
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
}
}
// check if a follow exists already
follows, err := p.db.Follows(requestingAccount, targetAcct)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
}
if follows {
// already follows so just return the relationship
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
}
// check if a follow exists already
followRequested, err := p.db.FollowRequested(requestingAccount, targetAcct)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
}
if followRequested {
// already follow requested so just return the relationship
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
}
// make the follow request
newFollowID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
fr := &gtsmodel.FollowRequest{
ID: newFollowID,
AccountID: requestingAccount.ID,
TargetAccountID: form.TargetAccountID,
ShowReblogs: true,
URI: util.GenerateURIForFollow(requestingAccount.Username, p.config.Protocol, p.config.Host, newFollowID),
Notify: false,
}
if form.Reblogs != nil {
fr.ShowReblogs = *form.Reblogs
}
if form.Notify != nil {
fr.Notify = *form.Notify
}
// whack it in the database
if err := p.db.Put(fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
}
// if it's a local account that's not locked we can just straight up accept the follow request
if !targetAcct.Locked && targetAcct.Domain == "" {
if _, err := p.db.AcceptFollowRequest(requestingAccount.ID, form.TargetAccountID); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
}
// return the new relationship
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
}
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: fr,
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
// return whatever relationship results from this
return p.RelationshipGet(requestingAccount, form.TargetAccountID)
}

View file

@ -0,0 +1,270 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"time"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// Delete handles the complete deletion of an account.
//
// TODO 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(account *gtsmodel.Account, deletedBy string) error {
l := p.log.WithFields(logrus.Fields{
"func": "Delete",
"username": account.Username,
})
l.Debugf("beginning account delete process for username %s", account.Username)
// 1. Delete account's application(s), clients, and oauth tokens
// we only need to do this step for local account since remote ones won't have any tokens or applications on our server
if account.Domain == "" {
// see if we can get a user for this account
u := &gtsmodel.User{}
if err := p.db.GetWhere([]db.Where{{Key: "account_id", Value: account.ID}}, u); err == nil {
// we got one! select all tokens with the user's ID
tokens := []*oauth.Token{}
if err := p.db.GetWhere([]db.Where{{Key: "user_id", Value: u.ID}}, &tokens); err == nil {
// we have some tokens to delete
for _, t := range tokens {
// delete client(s) associated with this token
if err := p.db.DeleteByID(t.ClientID, &oauth.Client{}); err != nil {
l.Errorf("error deleting oauth client: %s", err)
}
// delete application(s) associated with this token
if err := p.db.DeleteWhere([]db.Where{{Key: "client_id", Value: t.ClientID}}, &gtsmodel.Application{}); err != nil {
l.Errorf("error deleting application: %s", err)
}
// delete the token itself
if err := p.db.DeleteByID(t.ID, t); err != nil {
l.Errorf("error deleting oauth token: %s", err)
}
}
}
}
}
// 2. Delete account's blocks
l.Debug("deleting account blocks")
// first delete any blocks that this account created
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil {
l.Errorf("error deleting blocks created by account: %s", err)
}
// now delete any blocks that target this account
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil {
l.Errorf("error deleting blocks targeting account: %s", err)
}
// 3. Delete account's emoji
// nothing to do here
// 4. Delete account's follow requests
l.Debug("deleting account follow requests")
// first delete any follow requests that this account created
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
l.Errorf("error deleting follow requests created by account: %s", err)
}
// now delete any follow requests that target this account
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
l.Errorf("error deleting follow requests targeting account: %s", err)
}
// 5. Delete account's follows
l.Debug("deleting account follows")
// first delete any follows that this account created
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
l.Errorf("error deleting follows created by account: %s", err)
}
// now delete any follows that target this account
if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
l.Errorf("error deleting follows targeting account: %s", err)
}
// 6. Delete account's statuses
l.Debug("deleting account statuses")
// we'll select statuses 20 at a time so we don't wreck the db, and pass them through to the client api channel
// Deleting the statuses in this way also handles 7. Delete account's media attachments, 8. Delete account's mentions, and 9. Delete account's polls,
// since these are all attached to statuses.
var maxID string
selectStatusesLoop:
for {
statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// no statuses left for this instance so we're done
l.Infof("Delete: done iterating through statuses for account %s", account.Username)
break selectStatusesLoop
}
// an actual error has occurred
l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err)
break selectStatusesLoop
}
for i, s := range statuses {
// pass the status delete through the client api channel for processing
s.GTSAuthorAccount = account
l.Debug("putting status in the client api channel")
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsNote,
APActivityType: gtsmodel.ActivityStreamsDelete,
GTSModel: s,
OriginAccount: account,
TargetAccount: account,
}
if err := p.db.DeleteByID(s.ID, s); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// actual error has occurred
l.Errorf("Delete: db error status %s for account %s: %s", s.ID, account.Username, err)
break selectStatusesLoop
}
}
// if there are any boosts of this status, delete them as well
boosts := []*gtsmodel.Status{}
if err := p.db.GetWhere([]db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// an actual error has occurred
l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err)
break selectStatusesLoop
}
}
for _, b := range boosts {
oa := &gtsmodel.Account{}
if err := p.db.GetByID(b.AccountID, oa); err == nil {
l.Debug("putting boost undo in the client api channel")
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsAnnounce,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: s,
OriginAccount: oa,
TargetAccount: account,
}
}
if err := p.db.DeleteByID(b.ID, b); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// actual error has occurred
l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, err)
break selectStatusesLoop
}
}
}
// if this is the last status in the slice, set the maxID appropriately for the next query
if i == len(statuses)-1 {
maxID = s.ID
}
}
}
l.Debug("done deleting statuses")
// 10. Delete account's notifications
l.Debug("deleting account notifications")
if err := p.db.DeleteWhere([]db.Where{{Key: "origin_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil {
l.Errorf("error deleting notifications created by account: %s", err)
}
// 11. Delete account's bookmarks
l.Debug("deleting account bookmarks")
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
l.Errorf("error deleting bookmarks created by account: %s", err)
}
// 12. Delete account's faves
l.Debug("deleting account faves")
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusFave{}); err != nil {
l.Errorf("error deleting faves created by account: %s", err)
}
// 13. Delete account's mutes
l.Debug("deleting account mutes")
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusMute{}); err != nil {
l.Errorf("error deleting status mutes created by account: %s", err)
}
// 14. Delete account's streams
// 15. Delete account's tags
// TODO
// 16. Delete account's user
l.Debug("deleting account user")
if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &gtsmodel.User{}); err != nil {
return err
}
// 17. Delete account's timeline
// 18. Delete account itself
// to prevent the account being created again, set all these fields and update it in the db
// the account won't actually be *removed* from the database but it will be set to just a stub
account.Note = ""
account.DisplayName = ""
account.AvatarMediaAttachmentID = ""
account.AvatarRemoteURL = ""
account.HeaderMediaAttachmentID = ""
account.HeaderRemoteURL = ""
account.Reason = ""
account.Fields = []gtsmodel.Field{}
account.HideCollections = true
account.Discoverable = false
account.UpdatedAt = time.Now()
account.SuspendedAt = time.Now()
account.SuspensionOrigin = deletedBy
if err := p.db.UpdateByID(account.ID, account); err != nil {
return err
}
l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain)
return nil
}

View file

@ -0,0 +1,59 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) {
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, errors.New("account not found")
}
return nil, fmt.Errorf("db error: %s", err)
}
// lazily dereference things on the account if it hasn't been done yet
var requestingUsername string
if requestingAccount != nil {
requestingUsername = requestingAccount.Username
}
if err := p.federator.DereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
}
var mastoAccount *apimodel.Account
var err error
if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
} else {
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
}
if err != nil {
return nil, fmt.Errorf("error converting account: %s", err)
}
return mastoAccount, nil
}

View file

@ -0,0 +1,79 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
followers := []gtsmodel.Follow{}
accounts := []apimodel.Account{}
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range followers {
blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
a := &gtsmodel.Account{}
if err := p.db.GetByID(f.AccountID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
// derefence account fields in case we haven't done it already
if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
}
account, err := p.tc.AccountToMastoPublic(a)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
}

View file

@ -0,0 +1,79 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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) FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
following := []gtsmodel.Follow{}
accounts := []apimodel.Account{}
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return accounts, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, f := range following {
blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
continue
}
a := &gtsmodel.Account{}
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
continue
}
return nil, gtserror.NewErrorInternalError(err)
}
// derefence account fields in case we haven't done it already
if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
}
account, err := p.tc.AccountToMastoPublic(a)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
accounts = append(accounts, *account)
}
return accounts, nil
}

View file

@ -0,0 +1,46 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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(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(requestingAccount.ID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
}
r, err := p.tc.RelationshipToMasto(gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
}
return r, nil
}

View file

@ -0,0 +1,63 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
}
return nil, gtserror.NewErrorInternalError(err)
}
apiStatuses := []apimodel.Status{}
statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return apiStatuses, nil
}
return nil, gtserror.NewErrorInternalError(err)
}
for _, s := range statuses {
visible, err := p.filter.StatusVisible(s, requestingAccount)
if err != nil || !visible {
continue
}
apiStatus, err := p.tc.StatusToMasto(s, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
}
apiStatuses = append(apiStatuses, *apiStatus)
}
return apiStatuses, nil
}

View file

@ -0,0 +1,110 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"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) FollowRemove(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.Blocked(requestingAccount.ID, targetAccountID)
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 := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
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([]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(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([]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(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.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
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.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
}
// return whatever relationship results from all this
return p.RelationshipGet(requestingAccount, targetAccountID)
}

View file

@ -0,0 +1,199 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
l := p.log.WithField("func", "AccountUpdate")
if form.Discoverable != nil {
if err := p.db.UpdateOneByID(account.ID, "discoverable", *form.Discoverable, &gtsmodel.Account{}); err != nil {
return nil, fmt.Errorf("error updating discoverable: %s", err)
}
}
if form.Bot != nil {
if err := p.db.UpdateOneByID(account.ID, "bot", *form.Bot, &gtsmodel.Account{}); err != nil {
return nil, fmt.Errorf("error updating bot: %s", err)
}
}
if form.DisplayName != nil {
if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(account.ID, "display_name", *form.DisplayName, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Note != nil {
if err := util.ValidateNote(*form.Note); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(account.ID, "note", *form.Note, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Avatar != nil && form.Avatar.Size != 0 {
avatarInfo, err := p.UpdateAvatar(form.Avatar, account.ID)
if err != nil {
return nil, err
}
l.Tracef("new avatar info for account %s is %+v", account.ID, avatarInfo)
}
if form.Header != nil && form.Header.Size != 0 {
headerInfo, err := p.UpdateHeader(form.Header, account.ID)
if err != nil {
return nil, err
}
l.Tracef("new header info for account %s is %+v", account.ID, headerInfo)
}
if form.Locked != nil {
if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source != nil {
if form.Source.Language != nil {
if err := util.ValidateLanguage(*form.Source.Language); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(account.ID, "language", *form.Source.Language, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source.Sensitive != nil {
if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
if form.Source.Privacy != nil {
if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
return nil, err
}
if err := p.db.UpdateOneByID(account.ID, "privacy", *form.Source.Privacy, &gtsmodel.Account{}); err != nil {
return nil, err
}
}
}
// fetch the account with all updated values set
updatedAccount := &gtsmodel.Account{}
if err := p.db.GetByID(account.ID, updatedAccount); err != nil {
return nil, fmt.Errorf("could not fetch updated account %s: %s", account.ID, err)
}
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsUpdate,
GTSModel: updatedAccount,
OriginAccount: updatedAccount,
}
acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
if err != nil {
return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
}
return acctSensitive, nil
}
// 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(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error
if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
return nil, err
}
f, err := avatar.Open()
if err != nil {
return nil, fmt.Errorf("could not read provided avatar: %s", err)
}
// extract the bytes
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("could not read provided avatar: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided avatar: size 0 bytes")
}
// do the setting
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
if err != nil {
return nil, fmt.Errorf("error processing avatar: %s", err)
}
return avatarInfo, f.Close()
}
// 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(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error
if int(header.Size) > p.config.MediaConfig.MaxImageSize {
err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
return nil, err
}
f, err := header.Open()
if err != nil {
return nil, fmt.Errorf("could not read provided header: %s", err)
}
// extract the bytes
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("could not read provided header: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided header: size 0 bytes")
}
// do the setting
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
if err != nil {
return nil, fmt.Errorf("error processing header: %s", err)
}
return headerInfo, f.Close()
}

View file

@ -19,55 +19,27 @@
package processing
import (
"bytes"
"errors"
"fmt"
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
if !authed.User.Admin {
return nil, fmt.Errorf("user %s not an admin", authed.User.ID)
}
// open the emoji and extract the bytes from it
f, err := form.Image.Open()
if err != nil {
return nil, fmt.Errorf("error opening emoji: %s", err)
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("error reading emoji: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided emoji: size 0 bytes")
}
// allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
if err != nil {
return nil, fmt.Errorf("error reading emoji: %s", err)
}
emojiID, err := id.NewULID()
if err != nil {
return nil, err
}
emoji.ID = emojiID
mastoEmoji, err := p.tc.EmojiToMasto(emoji)
if err != nil {
return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
}
if err := p.db.Put(emoji); err != nil {
return nil, fmt.Errorf("database error while processing emoji: %s", err)
}
return &mastoEmoji, nil
return p.adminProcessor.EmojiCreate(authed.Account, authed.User, form)
}
func (p *processor) AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockCreate(authed.Account, form)
}
func (p *processor) AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlocksGet(authed.Account, export)
}
func (p *processor) AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockGet(authed.Account, id, export)
}
func (p *processor) AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockDelete(authed.Account, id)
}

View file

@ -0,0 +1,60 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"github.com/sirupsen/logrus"
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/media"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// Processor wraps a bunch of functions for processing admin actions.
type Processor interface {
DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
DomainBlockGet(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
}
type processor struct {
tc typeutils.TypeConverter
config *config.Config
mediaHandler media.Handler
fromClientAPI chan gtsmodel.FromClientAPI
db db.DB
log *logrus.Logger
}
// New returns a new admin processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan gtsmodel.FromClientAPI, config *config.Config, log *logrus.Logger) Processor {
return &processor{
tc: tc,
config: config,
mediaHandler: mediaHandler,
fromClientAPI: fromClientAPI,
db: db,
log: log,
}
}

View file

@ -0,0 +1,154 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"fmt"
"time"
"github.com/sirupsen/logrus"
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"
)
func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
// first check if we already have a block -- if err == nil we already had a block so we can skip a whole lot of work
domainBlock := &gtsmodel.DomainBlock{}
err := p.db.GetWhere([]db.Where{{Key: "domain", Value: form.Domain, CaseInsensitive: true}}, domainBlock)
if err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// something went wrong in the DB
return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", form.Domain, err))
}
// there's no block for this domain yet so create one
// note: we take a new ulid from timestamp here in case we need to sort blocks
blockID, err := id.NewULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error creating id for new domain block %s: %s", form.Domain, err))
}
domainBlock = &gtsmodel.DomainBlock{
ID: blockID,
Domain: form.Domain,
CreatedByAccountID: account.ID,
PrivateComment: form.PrivateComment,
PublicComment: form.PublicComment,
Obfuscate: form.Obfuscate,
}
// put the new block in the database
if err := p.db.Put(domainBlock); err != nil {
if _, ok := err.(db.ErrAlreadyExists); !ok {
// there's a real error creating the block
return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error putting new domain block %s: %s", form.Domain, err))
}
}
// process the side effects of the domain block asynchronously since it might take a while
go p.initiateDomainBlockSideEffects(account, domainBlock) // TODO: add this to a queuing system so it can retry/resume
}
mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error converting domain block to frontend/masto representation %s: %s", form.Domain, err))
}
return mastoDomainBlock, nil
}
// initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block:
//
// 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(account *gtsmodel.Account, block *gtsmodel.DomainBlock) {
l := p.log.WithFields(logrus.Fields{
"func": "domainBlockProcessSideEffects",
"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
instance := &gtsmodel.Instance{}
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: block.Domain, CaseInsensitive: true}}, instance); err == nil {
instance.Title = ""
instance.UpdatedAt = time.Now()
instance.SuspendedAt = time.Now()
instance.DomainBlockID = block.ID
instance.ShortDescription = ""
instance.Description = ""
instance.Terms = ""
instance.ContactEmail = ""
instance.ContactAccountUsername = ""
instance.ContactAccountID = ""
instance.Version = ""
if err := p.db.UpdateByID(instance.ID, instance); err != nil {
l.Errorf("domainBlockProcessSideEffects: db error updating instance: %s", err)
}
l.Debug("domainBlockProcessSideEffects: instance entry updated")
}
// if we have an instance account for this instance, delete it
if err := p.db.DeleteWhere([]db.Where{{Key: "username", Value: block.Domain, CaseInsensitive: true}}, &gtsmodel.Account{}); err != nil {
l.Errorf("domainBlockProcessSideEffects: db error removing instance account: %s", err)
}
// delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines)
limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query
var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID)
selectAccountsLoop:
for {
accounts, err := p.db.GetAccountsForInstance(block.Domain, maxID, limit)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// no accounts left for this instance so we're done
l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain)
break selectAccountsLoop
}
// an actual error has occurred
l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err)
break selectAccountsLoop
}
for i, a := range accounts {
l.Debugf("putting delete for account %s in the clientAPI channel", a.Username)
// pass the account delete through the client api channel for processing
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsPerson,
APActivityType: gtsmodel.ActivityStreamsDelete,
GTSModel: a,
OriginAccount: account,
TargetAccount: a,
}
// if this is the last account in the slice, set the maxID appropriately for the next query
if i == len(accounts)-1 {
maxID = a.ID
}
}
}
}

View file

@ -0,0 +1,36 @@
package admin
import (
"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) DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(id, domainBlock); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// 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
mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// delete the domain block
if err := p.db.DeleteByID(id, domainBlock); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return mastoDomainBlock, nil
}

View file

@ -0,0 +1,73 @@
/*
GoToSocial
Copyright (C) 2021 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"
"errors"
"fmt"
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
)
func (p *processor) EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
if user.Admin {
return nil, fmt.Errorf("user %s not an admin", user.ID)
}
// open the emoji and extract the bytes from it
f, err := form.Image.Open()
if err != nil {
return nil, fmt.Errorf("error opening emoji: %s", err)
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("error reading emoji: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided emoji: size 0 bytes")
}
// allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
if err != nil {
return nil, fmt.Errorf("error reading emoji: %s", err)
}
emojiID, err := id.NewULID()
if err != nil {
return nil, err
}
emoji.ID = emojiID
mastoEmoji, err := p.tc.EmojiToMasto(emoji)
if err != nil {
return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
}
if err := p.db.Put(emoji); err != nil {
return nil, fmt.Errorf("database error while processing emoji: %s", err)
}
return &mastoEmoji, nil
}

View file

@ -0,0 +1,30 @@
package admin
import (
"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(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
domainBlock := &gtsmodel.DomainBlock{}
if err := p.db.GetByID(id, domainBlock); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// 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))
}
mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return mastoDomainBlock, nil
}

View file

@ -0,0 +1,30 @@
package admin
import (
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(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
domainBlocks := []*gtsmodel.DomainBlock{}
if err := p.db.GetAll(&domainBlocks); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// something has gone really wrong
return nil, gtserror.NewErrorInternalError(err)
}
}
mastoDomainBlocks := []*apimodel.DomainBlock{}
for _, b := range domainBlocks {
mastoDomainBlock, err := p.tc.DomainBlockToMasto(b, export)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
mastoDomainBlocks = append(mastoDomainBlocks, mastoDomainBlock)
}
return mastoDomainBlocks, nil
}

View file

@ -20,6 +20,7 @@ package processing
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -89,7 +90,7 @@ func (p *processor) dereferenceFediRequest(username string, requestingAccountURI
return requestingAccount, nil
}
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -98,17 +99,17 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
var requestedPerson vocab.ActivityStreamsPerson
var err error
if util.IsPublicKeyPath(request.URL) {
if util.IsPublicKeyPath(requestURL) {
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
} else if util.IsUserPath(request.URL) {
} else if util.IsUserPath(requestURL) {
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
// if we're already handshaking/dereferencing a remote account, we can skip the dereferencing part
@ -144,7 +145,7 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
return data, nil
}
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -152,9 +153,9 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@ -189,7 +190,7 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
return data, nil
}
func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -197,9 +198,9 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@ -234,7 +235,7 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
return data, nil
}
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) {
func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -242,9 +243,9 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@ -294,7 +295,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
return data, nil
}
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) {
func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -356,6 +357,5 @@ func (p *processor) GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtse
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
return posted, err
return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
}

View file

@ -25,6 +25,7 @@ import (
"net/url"
"github.com/go-fed/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
@ -161,17 +162,69 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
return errors.New("note was not parseable as *gtsmodel.Status")
}
if statusToDelete.GTSAuthorAccount == nil {
statusToDelete.GTSAuthorAccount = clientMsg.OriginAccount
}
// delete all attachments for this status
for _, a := range statusToDelete.Attachments {
if err := p.mediaProcessor.Delete(a); err != nil {
return err
}
}
// delete all mentions for this status
for _, m := range statusToDelete.Mentions {
if err := p.db.DeleteByID(m, &gtsmodel.Mention{}); err != nil {
return err
}
}
// delete all notifications for this status
if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
return err
}
// delete this status from any and all timelines
if err := p.deleteStatusFromTimelines(statusToDelete); err != nil {
return err
}
return p.federateStatusDelete(statusToDelete, clientMsg.OriginAccount)
return p.federateStatusDelete(statusToDelete)
case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson:
// DELETE ACCOUNT/PROFILE
accountToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return errors.New("account was not parseable as *gtsmodel.Account")
}
var deletedBy string
if clientMsg.OriginAccount != nil {
deletedBy = clientMsg.OriginAccount.ID
}
return p.accountProcessor.Delete(accountToDelete, deletedBy)
}
}
return nil
}
// TODO: move all the below functions into federation.Federator
func (p *processor) federateStatus(status *gtsmodel.Status) error {
if status.GTSAuthorAccount == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(status.AccountID, a); err != nil {
return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
}
status.GTSAuthorAccount = a
}
// do nothing if this isn't our status
if status.GTSAuthorAccount.Domain != "" {
return nil
}
asStatus, err := p.tc.StatusToAS(status)
if err != nil {
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
@ -186,20 +239,33 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
return err
}
func (p *processor) federateStatusDelete(status *gtsmodel.Status, originAccount *gtsmodel.Account) error {
func (p *processor) federateStatusDelete(status *gtsmodel.Status) error {
if status.GTSAuthorAccount == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(status.AccountID, a); err != nil {
return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
}
status.GTSAuthorAccount = a
}
// do nothing if this isn't our status
if status.GTSAuthorAccount.Domain != "" {
return nil
}
asStatus, err := p.tc.StatusToAS(status)
if err != nil {
return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)
}
outboxIRI, err := url.Parse(originAccount.OutboxURI)
outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)
if err != nil {
return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err)
}
actorIRI, err := url.Parse(originAccount.URI)
actorIRI, err := url.Parse(status.GTSAuthorAccount.URI)
if err != nil {
return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", originAccount.URI, err)
return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.GTSAuthorAccount.URI, err)
}
// create a delete and set the appropriate actor on it
@ -326,6 +392,11 @@ func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gts
}
func (p *processor) federateUnannounce(boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
if originAccount.Domain != "" {
// nothing to do here
return nil
}
asAnnounce, err := p.tc.BoostToAS(boost, originAccount, targetAccount)
if err != nil {
return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err)

View file

@ -21,7 +21,6 @@ package processing
import (
"errors"
"fmt"
"net/url"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -49,7 +48,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming status")
if err := p.dereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
return fmt.Errorf("error dereferencing status from federator: %s", err)
}
if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
@ -72,7 +71,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming account")
if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err)
}
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
@ -105,7 +104,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return errors.New("announce was not parseable as *gtsmodel.Status")
}
if err := p.dereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
if err := p.federator.DereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
return fmt.Errorf("error dereferencing announce from federator: %s", err)
}
@ -140,7 +139,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming account")
if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err)
}
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
@ -160,6 +159,27 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
}
// delete all attachments for this status
for _, a := range statusToDelete.Attachments {
if err := p.mediaProcessor.Delete(a); err != nil {
return err
}
}
// delete all mentions for this status
for _, m := range statusToDelete.Mentions {
if err := p.db.DeleteByID(m, &gtsmodel.Mention{}); err != nil {
return err
}
}
// delete all notifications for this status
if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
return err
}
// remove this status from any and all timelines
return p.deleteStatusFromTimelines(statusToDelete)
case gtsmodel.ActivityStreamsProfile:
// DELETE A PROFILE/ACCOUNT
@ -183,299 +203,3 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return nil
}
// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
// federated status, back in the federating db's Create function.
//
// When a status comes in from the federation API, there are certain fields that
// haven't been dereferenced yet, because we needed to provide a snappy synchronous
// response to the caller. By the time it reaches this function though, it's being
// processed asynchronously, so we have all the time in the world to fetch the various
// bits and bobs that are attached to the status, and properly flesh it out, before we
// send the status to any timelines and notify people.
//
// Things to dereference and fetch here:
//
// 1. Media attachments.
// 2. Hashtags.
// 3. Emojis.
// 4. Mentions.
// 5. Posting account.
// 6. Replied-to-status.
//
// SIDE EFFECTS:
// This function will deference all of the above, insert them in the database as necessary,
// and attach them to the status. The status itself will not be added to the database yet,
// that's up the caller to do.
func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
l := p.log.WithFields(logrus.Fields{
"func": "dereferenceStatusFields",
"status": fmt.Sprintf("%+v", status),
})
l.Debug("entering function")
t, err := p.federator.GetTransportForUser(requestingUsername)
if err != nil {
return fmt.Errorf("error creating transport: %s", err)
}
// the status should have an ID by now, but just in case it doesn't let's generate one here
// because we'll need it further down
if status.ID == "" {
newID, err := id.NewULIDFromTime(status.CreatedAt)
if err != nil {
return err
}
status.ID = newID
}
// 1. Media attachments.
//
// At this point we should know:
// * the media type of the file we're looking for (a.File.ContentType)
// * the blurhash (a.Blurhash)
// * the file type (a.Type)
// * the remote URL (a.RemoteURL)
// This should be enough to pass along to the media processor.
attachmentIDs := []string{}
for _, a := range status.GTSMediaAttachments {
l.Debugf("dereferencing attachment: %+v", a)
// it might have been processed elsewhere so check first if it's already in the database or not
maybeAttachment := &gtsmodel.MediaAttachment{}
err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
if err == nil {
// we already have it in the db, dereferenced, no need to do it again
l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
continue
}
if _, ok := err.(db.ErrNoEntries); !ok {
// we have a real error
return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
}
// it just doesn't exist yet so carry on
l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
if err != nil {
p.log.Errorf("error dereferencing status attachment: %s", err)
continue
}
l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
deferencedAttachment.StatusID = status.ID
deferencedAttachment.Description = a.Description
if err := p.db.Put(deferencedAttachment); err != nil {
return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
}
attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
}
status.Attachments = attachmentIDs
// 2. Hashtags
// 3. Emojis
// 4. Mentions
// At this point, mentions should have the namestring and mentionedAccountURI set on them.
//
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
mentions := []string{}
for _, m := range status.GTSMentions {
if m.ID == "" {
mID, err := id.NewRandomULID()
if err != nil {
return err
}
m.ID = mID
}
uri, err := url.Parse(m.MentionedAccountURI)
if err != nil {
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
continue
}
m.StatusID = status.ID
m.OriginAccountID = status.GTSAuthorAccount.ID
m.OriginAccountURI = status.GTSAuthorAccount.URI
targetAccount := &gtsmodel.Account{}
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
// proper error
if _, ok := err.(db.ErrNoEntries); !ok {
return fmt.Errorf("db error checking for account with uri %s", uri.String())
}
// we just don't have it yet, so we should go get it....
accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, uri)
if err != nil {
// we can't dereference it so just skip it
l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
continue
}
targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
if err != nil {
l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
continue
}
targetAccountID, err := id.NewRandomULID()
if err != nil {
return err
}
targetAccount.ID = targetAccountID
if err := p.db.Put(targetAccount); err != nil {
return fmt.Errorf("db error inserting account with uri %s", uri.String())
}
}
// by this point, we know the targetAccount exists in our database with an ID :)
m.TargetAccountID = targetAccount.ID
if err := p.db.Put(m); err != nil {
return fmt.Errorf("error creating mention: %s", err)
}
mentions = append(mentions, m.ID)
}
status.Mentions = mentions
return nil
}
func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
l := p.log.WithFields(logrus.Fields{
"func": "dereferenceAccountFields",
"requestingUsername": requestingUsername,
})
t, err := p.federator.GetTransportForUser(requestingUsername)
if err != nil {
return fmt.Errorf("error getting transport for user: %s", err)
}
// fetch the header and avatar
if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
// if this doesn't work, just skip it -- we can do it later
l.Debugf("error fetching header/avi for account: %s", err)
}
if err := p.db.UpdateByID(account.ID, account); err != nil {
return fmt.Errorf("error updating account in database: %s", err)
}
return nil
}
func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
// we can't do anything unfortunately
return errors.New("dereferenceAnnounce: no URI to dereference")
}
// check if we already have the boosted status in the database
boostedStatus := &gtsmodel.Status{}
err := p.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
if err == nil {
// nice, we already have it so we don't actually need to dereference it from remote
announce.Content = boostedStatus.Content
announce.ContentWarning = boostedStatus.ContentWarning
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
announce.Sensitive = boostedStatus.Sensitive
announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID
announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus
return nil
}
// we don't have it so we need to dereference it
remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
}
statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
}
// make sure we have the author account in the db
attributedToProp := statusable.GetActivityStreamsAttributedTo()
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
accountURI := iter.GetIRI()
if accountURI == nil {
continue
}
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, &gtsmodel.Account{}); err == nil {
// we already have it, fine
continue
}
// we don't have the boosted status author account yet so dereference it
accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, accountURI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
}
account, err := p.tc.ASRepresentationToAccount(accountable, false)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
}
accountID, err := id.NewRandomULID()
if err != nil {
return err
}
account.ID = accountID
if err := p.db.Put(account); err != nil {
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
}
if err := p.dereferenceAccountFields(account, requestingUsername, false); err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
}
}
// now convert the statusable into something we can understand
boostedStatus, err = p.tc.ASStatusToStatus(statusable)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
}
boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
if err != nil {
return nil
}
boostedStatus.ID = boostedStatusID
if err := p.db.Put(boostedStatus); err != nil {
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
}
// now dereference additional fields straight away (we're already async here so we have time)
if err := p.dereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
}
// update with the newly dereferenced fields
if err := p.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
}
// we have everything we need!
announce.Content = boostedStatus.Content
announce.ContentWarning = boostedStatus.ContentWarning
announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
announce.Sensitive = boostedStatus.Sensitive
announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID
announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus
return nil
}

View file

@ -131,7 +131,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
// process avatar if provided
if form.Avatar != nil && form.Avatar.Size != 0 {
_, err := p.updateAccountAvatar(form.Avatar, ia.ID)
_, err := p.accountProcessor.UpdateAvatar(form.Avatar, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
}
@ -139,7 +139,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
// process header if provided
if form.Header != nil && form.Header.Size != 0 {
_, err := p.updateAccountHeader(form.Header, ia.ID)
_, err := p.accountProcessor.UpdateHeader(form.Header, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
}

View file

@ -19,268 +19,23 @@
package processing
import (
"bytes"
"errors"
"fmt"
"io"
"strconv"
"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/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
// First check this user/account is permitted to create media
// There's no point continuing otherwise.
//
// TODO: move this check to the oauth.Authed function and do it for all accounts
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
return nil, errors.New("not authorized to post new media")
}
// open the attachment and extract the bytes from it
f, err := form.File.Open()
if err != nil {
return nil, fmt.Errorf("error opening attachment: %s", err)
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("error reading attachment: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided attachment: size 0 bytes")
}
// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "")
if err != nil {
return nil, fmt.Errorf("error reading attachment: %s", err)
}
// now we need to add extra fields that the attachment processor doesn't know (from the form)
// TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
// first description
attachment.Description = form.Description
// now parse the focus parameter
focusx, focusy, err := parseFocus(form.Focus)
if err != nil {
return nil, err
}
attachment.FileMeta.Focus.X = focusx
attachment.FileMeta.Focus.Y = focusy
// prepare the frontend representation now -- if there are any errors here at least we can bail without
// having already put something in the database and then having to clean it up again (eugh)
mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
}
// now we can confidently put the attachment in the database
if err := p.db.Put(attachment); err != nil {
return nil, fmt.Errorf("error storing media attachment in db: %s", err)
}
return &mastoAttachment, nil
return p.mediaProcessor.Create(authed.Account, form)
}
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
attachment := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// attachment doesn't exist
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
}
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
}
if attachment.AccountID != authed.Account.ID {
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
}
a, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
}
return &a, nil
return p.mediaProcessor.GetMedia(authed.Account, mediaAttachmentID)
}
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
attachment := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// attachment doesn't exist
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
}
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
}
if attachment.AccountID != authed.Account.ID {
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
}
if form.Description != nil {
attachment.Description = *form.Description
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
}
}
if form.Focus != nil {
focusx, focusy, err := parseFocus(*form.Focus)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err)
}
attachment.FileMeta.Focus.X = focusx
attachment.FileMeta.Focus.Y = focusy
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
}
}
a, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
}
return &a, nil
return p.mediaProcessor.Update(authed.Account, mediaAttachmentID, form)
}
func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
// parse the form fields
mediaSize, err := media.ParseMediaSize(form.MediaSize)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
}
mediaType, err := media.ParseMediaType(form.MediaType)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
}
spl := strings.Split(form.FileName, ".")
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
}
wantedMediaID := spl[0]
// get the account that owns the media and make sure it's not suspended
acct := &gtsmodel.Account{}
if err := p.db.GetByID(form.AccountID, acct); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
}
if !acct.SuspendedAt.IsZero() {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
}
// make sure the requesting account and the media account don't block each other
if authed.Account != nil {
blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
}
}
// the way we store emojis is a little different from the way we store other attachments,
// so we need to take different steps depending on the media type being requested
content := &apimodel.Content{}
var storagePath string
switch mediaType {
case media.Emoji:
e := &gtsmodel.Emoji{}
if err := p.db.GetByID(wantedMediaID, e); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
}
if e.Disabled {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
}
switch mediaSize {
case media.Original:
content.ContentType = e.ImageContentType
storagePath = e.ImagePath
case media.Static:
content.ContentType = e.ImageStaticContentType
storagePath = e.ImageStaticPath
default:
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
}
case media.Attachment, media.Header, media.Avatar:
a := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(wantedMediaID, a); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
}
if a.AccountID != form.AccountID {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
}
switch mediaSize {
case media.Original:
content.ContentType = a.File.ContentType
storagePath = a.File.Path
case media.Small:
content.ContentType = a.Thumbnail.ContentType
storagePath = a.Thumbnail.Path
default:
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
}
}
bytes, err := p.storage.RetrieveFileFrom(storagePath)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
}
content.ContentLength = int64(len(bytes))
content.Content = bytes
return content, nil
}
func parseFocus(focus string) (focusx, focusy float32, err error) {
if focus == "" {
return
}
spl := strings.Split(focus, ",")
if len(spl) != 2 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
xStr := spl[0]
yStr := spl[1]
if xStr == "" || yStr == "" {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
fx, err := strconv.ParseFloat(xStr, 32)
if err != nil {
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
return
}
if fx > 1 || fx < -1 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
focusx = float32(fx)
fy, err := strconv.ParseFloat(yStr, 32)
if err != nil {
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
return
}
if fy > 1 || fy < -1 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
focusy = float32(fy)
return
return p.mediaProcessor.GetFile(authed.Account, form)
}

View file

@ -0,0 +1,79 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"bytes"
"errors"
"fmt"
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
// open the attachment and extract the bytes from it
f, err := form.File.Open()
if err != nil {
return nil, fmt.Errorf("error opening attachment: %s", err)
}
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("error reading attachment: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided attachment: size 0 bytes")
}
// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), account.ID, "")
if err != nil {
return nil, fmt.Errorf("error reading attachment: %s", err)
}
// now we need to add extra fields that the attachment processor doesn't know (from the form)
// TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
// first description
attachment.Description = form.Description
// now parse the focus parameter
focusx, focusy, err := parseFocus(form.Focus)
if err != nil {
return nil, err
}
attachment.FileMeta.Focus.X = focusx
attachment.FileMeta.Focus.Y = focusy
// prepare the frontend representation now -- if there are any errors here at least we can bail without
// having already put something in the database and then having to clean it up again (eugh)
mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
}
// now we can confidently put the attachment in the database
if err := p.db.Put(attachment); err != nil {
return nil, fmt.Errorf("error storing media attachment in db: %s", err)
}
return &mastoAttachment, nil
}

View file

@ -0,0 +1,51 @@
package media
import (
"fmt"
"strings"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode {
a := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(mediaAttachmentID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// attachment already gone
return nil
}
// actual error
return gtserror.NewErrorInternalError(err)
}
errs := []string{}
// delete the thumbnail from storage
if a.Thumbnail.Path != "" {
if err := p.storage.RemoveFileAt(a.Thumbnail.Path); err != nil {
errs = append(errs, fmt.Sprintf("remove thumbnail at path %s: %s", a.Thumbnail.Path, err))
}
}
// delete the file from storage
if a.File.Path != "" {
if err := p.storage.RemoveFileAt(a.File.Path); err != nil {
errs = append(errs, fmt.Sprintf("remove file at path %s: %s", a.File.Path, err))
}
}
// delete the attachment
if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
errs = append(errs, fmt.Sprintf("remove attachment: %s", err))
}
}
if len(errs) != 0 {
return gtserror.NewErrorInternalError(fmt.Errorf("Delete: one or more errors removing attachment with id %s: %s", mediaAttachmentID, strings.Join(errs, "; ")))
}
return nil
}

View file

@ -0,0 +1,120 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"fmt"
"strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
)
func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
// parse the form fields
mediaSize, err := media.ParseMediaSize(form.MediaSize)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
}
mediaType, err := media.ParseMediaType(form.MediaType)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
}
spl := strings.Split(form.FileName, ".")
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
}
wantedMediaID := spl[0]
// get the account that owns the media and make sure it's not suspended
acct := &gtsmodel.Account{}
if err := p.db.GetByID(form.AccountID, acct); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
}
if !acct.SuspendedAt.IsZero() {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
}
// make sure the requesting account and the media account don't block each other
if account != nil {
blocked, err := p.db.Blocked(account.ID, form.AccountID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err))
}
if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, account.ID))
}
}
// the way we store emojis is a little different from the way we store other attachments,
// so we need to take different steps depending on the media type being requested
content := &apimodel.Content{}
var storagePath string
switch mediaType {
case media.Emoji:
e := &gtsmodel.Emoji{}
if err := p.db.GetByID(wantedMediaID, e); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
}
if e.Disabled {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
}
switch mediaSize {
case media.Original:
content.ContentType = e.ImageContentType
storagePath = e.ImagePath
case media.Static:
content.ContentType = e.ImageStaticContentType
storagePath = e.ImageStaticPath
default:
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
}
case media.Attachment, media.Header, media.Avatar:
a := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(wantedMediaID, a); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
}
if a.AccountID != form.AccountID {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
}
switch mediaSize {
case media.Original:
content.ContentType = a.File.ContentType
storagePath = a.File.Path
case media.Small:
content.ContentType = a.Thumbnail.ContentType
storagePath = a.Thumbnail.Path
default:
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
}
}
bytes, err := p.storage.RetrieveFileFrom(storagePath)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
}
content.ContentLength = int64(len(bytes))
content.Content = bytes
return content, nil
}

View file

@ -0,0 +1,51 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"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) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
attachment := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// attachment doesn't exist
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
}
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
}
if attachment.AccountID != account.ID {
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
}
a, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
}
return &a, nil
}

View file

@ -0,0 +1,63 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/blob"
"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/media"
"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(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
Delete(mediaAttachmentID string) gtserror.WithCode
GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode)
Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
}
type processor struct {
tc typeutils.TypeConverter
config *config.Config
mediaHandler media.Handler
storage blob.Storage
db db.DB
log *logrus.Logger
}
// New returns a new media processor.
func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, storage blob.Storage, config *config.Config, log *logrus.Logger) Processor {
return &processor{
tc: tc,
config: config,
mediaHandler: mediaHandler,
storage: storage,
db: db,
log: log,
}
}

View file

@ -0,0 +1,70 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"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) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
attachment := &gtsmodel.MediaAttachment{}
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// attachment doesn't exist
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
}
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
}
if attachment.AccountID != account.ID {
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
}
if form.Description != nil {
attachment.Description = *form.Description
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
}
}
if form.Focus != nil {
focusx, focusy, err := parseFocus(*form.Focus)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err)
}
attachment.FileMeta.Focus.X = focusx
attachment.FileMeta.Focus.Y = focusy
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
}
}
a, err := p.tc.AttachmentToMasto(attachment)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
}
return &a, nil
}

View file

@ -0,0 +1,63 @@
/*
GoToSocial
Copyright (C) 2021 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 media
import (
"fmt"
"strconv"
"strings"
)
func parseFocus(focus string) (focusx, focusy float32, err error) {
if focus == "" {
return
}
spl := strings.Split(focus, ",")
if len(spl) != 2 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
xStr := spl[0]
yStr := spl[1]
if xStr == "" || yStr == "" {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
fx, err := strconv.ParseFloat(xStr, 32)
if err != nil {
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
return
}
if fx > 1 || fx < -1 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
focusx = float32(fx)
fy, err := strconv.ParseFloat(yStr, 32)
if err != nil {
err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
return
}
if fy > 1 || fy < -1 {
err = fmt.Errorf("improperly formatted focus %s", focus)
return
}
focusy = float32(fy)
return
}

View file

@ -21,6 +21,7 @@ package processing
import (
"context"
"net/http"
"net/url"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@ -32,8 +33,11 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status"
"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/streaming"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
"github.com/superseriousbusiness/gotosocial/internal/processing/streaming"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
@ -81,6 +85,14 @@ type Processor interface {
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
// AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form.
AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlocksGet returns a list of currently blocked domains.
AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
// AdminDomainBlockGet returns one domain block, specified by ID.
AdminDomainBlockGet(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(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode)
// AppCreate processes the creation of a new API application
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
@ -154,22 +166,22 @@ type Processor interface {
// 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(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
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(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
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(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
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(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode)
GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID 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(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetNodeInfoRel returns a well known response giving the path to node info.
GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
@ -210,8 +222,11 @@ type processor struct {
SUB-PROCESSORS
*/
accountProcessor account.Processor
adminProcessor admin.Processor
statusProcessor status.Processor
streamingProcessor streaming.Processor
mediaProcessor mediaProcessor.Processor
}
// NewProcessor returns a new Processor that uses the given federator and logger
@ -222,6 +237,9 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
statusProcessor := status.New(db, tc, config, fromClientAPI, log)
streamingProcessor := streaming.New(db, tc, oauthServer, config, log)
accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator, config, log)
adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config, log)
mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config, log)
return &processor{
fromClientAPI: fromClientAPI,
@ -238,8 +256,11 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
db: db,
filter: visibility.NewFilter(db, log),
accountProcessor: accountProcessor,
adminProcessor: adminProcessor,
statusProcessor: statusProcessor,
streamingProcessor: streamingProcessor,
mediaProcessor: mediaProcessor,
}
}
@ -251,14 +272,18 @@ func (p *processor) Start() error {
select {
case clientMsg := <-p.fromClientAPI:
p.log.Infof("received message FROM client API: %+v", clientMsg)
if err := p.processFromClientAPI(clientMsg); err != nil {
p.log.Error(err)
}
go func() {
if err := p.processFromClientAPI(clientMsg); err != nil {
p.log.Error(err)
}
}()
case federatorMsg := <-p.fromFederator:
p.log.Infof("received message FROM federator: %+v", federatorMsg)
if err := p.processFromFederator(federatorMsg); err != nil {
p.log.Error(err)
}
go func() {
if err := p.processFromFederator(federatorMsg); err != nil {
p.log.Error(err)
}
}()
case <-p.stop:
break DistLoop
}

View file

@ -176,7 +176,7 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve
}
// properly dereference everything in the status (media attachments etc)
if err := p.dereferenceStatusFields(status, authed.Account.Username); err != nil {
if err := p.federator.DereferenceStatusFields(status, authed.Account.Username); err != nil {
return nil, fmt.Errorf("error dereferencing status fields: %s", err)
}
@ -223,7 +223,7 @@ func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
}
if err := p.dereferenceAccountFields(account, authed.Account.Username, false); err != nil {
if err := p.federator.DereferenceAccountFields(account, authed.Account.Username, false); err != nil {
return nil, fmt.Errorf("searchAccountByURI: error further dereferencing account with uri %s: %s", uri.String(), err)
}
@ -301,7 +301,7 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r
}
// properly dereference all the fields on the account immediately
if err := p.dereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
if err := p.federator.DereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
return nil, fmt.Errorf("searchAccountByMention: error dereferencing fields on account with uri %s: %s", acctURI.String(), err)
}
}

View file

@ -49,6 +49,7 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a
APActivityType: gtsmodel.ActivityStreamsDelete,
GTSModel: targetStatus,
OriginAccount: account,
TargetAccount: account,
}
return mastoStatus, nil

View file

@ -1,135 +0,0 @@
/*
GoToSocial
Copyright (C) 2021 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 (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
/*
HELPER FUNCTIONS
*/
// TODO: try to combine the below two functions because this is a lot of code repetition.
// updateAccountAvatar 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) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error
if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
return nil, err
}
f, err := avatar.Open()
if err != nil {
return nil, fmt.Errorf("could not read provided avatar: %s", err)
}
// extract the bytes
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("could not read provided avatar: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided avatar: size 0 bytes")
}
// do the setting
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
if err != nil {
return nil, fmt.Errorf("error processing avatar: %s", err)
}
return avatarInfo, f.Close()
}
// updateAccountHeader 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) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
var err error
if int(header.Size) > p.config.MediaConfig.MaxImageSize {
err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
return nil, err
}
f, err := header.Open()
if err != nil {
return nil, fmt.Errorf("could not read provided header: %s", err)
}
// extract the bytes
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
return nil, fmt.Errorf("could not read provided header: %s", err)
}
if size == 0 {
return nil, errors.New("could not read provided header: size 0 bytes")
}
// do the setting
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
if err != nil {
return nil, fmt.Errorf("error processing header: %s", err)
}
return headerInfo, f.Close()
}
// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
// on behalf of requestingUsername.
//
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
//
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
// to reflect the creation of these new attachments.
func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{
RemoteURL: targetAccount.AvatarRemoteURL,
Avatar: true,
}, targetAccount.ID)
if err != nil {
return fmt.Errorf("error processing avatar for user: %s", err)
}
targetAccount.AvatarMediaAttachmentID = a.ID
}
if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{
RemoteURL: targetAccount.HeaderRemoteURL,
Header: true,
}, targetAccount.ID)
if err != nil {
return fmt.Errorf("error processing header for user: %s", err)
}
targetAccount.HeaderMediaAttachmentID = a.ID
}
return nil
}