51 changed files with 2280 additions and 640 deletions
@ -0,0 +1,119 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package account |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap" |
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" |
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/superseriousbusiness/gotosocial/internal/messages" |
||||
"github.com/superseriousbusiness/gotosocial/internal/paging" |
||||
) |
||||
|
||||
// FollowRequestAccept handles the accepting of a follow request from the sourceAccountID to the requestingAccount (the currently authorized account).
|
||||
func (p *Processor) FollowRequestAccept(ctx context.Context, requestingAccount *gtsmodel.Account, sourceAccountID string) (*apimodel.Relationship, gtserror.WithCode) { |
||||
follow, err := p.state.DB.AcceptFollowRequest(ctx, sourceAccountID, requestingAccount.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
if follow.Account != nil { |
||||
// Only enqueue work in the case we have a request creating account stored.
|
||||
// NOTE: due to how AcceptFollowRequest works, the inverse shouldn't be possible.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ |
||||
APObjectType: ap.ActivityFollow, |
||||
APActivityType: ap.ActivityAccept, |
||||
GTSModel: follow, |
||||
OriginAccount: follow.Account, |
||||
TargetAccount: follow.TargetAccount, |
||||
}) |
||||
} |
||||
|
||||
return p.RelationshipGet(ctx, requestingAccount, sourceAccountID) |
||||
} |
||||
|
||||
// FollowRequestReject handles the rejection of a follow request from the sourceAccountID to the requestingAccount (the currently authorized account).
|
||||
func (p *Processor) FollowRequestReject(ctx context.Context, requestingAccount *gtsmodel.Account, sourceAccountID string) (*apimodel.Relationship, gtserror.WithCode) { |
||||
followRequest, err := p.state.DB.GetFollowRequest(ctx, sourceAccountID, requestingAccount.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
err = p.state.DB.RejectFollowRequest(ctx, sourceAccountID, requestingAccount.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
if followRequest.Account != nil { |
||||
// Only enqueue work in the case we have a request creating account stored.
|
||||
// NOTE: due to how GetFollowRequest works, the inverse shouldn't be possible.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ |
||||
APObjectType: ap.ActivityFollow, |
||||
APActivityType: ap.ActivityReject, |
||||
GTSModel: followRequest, |
||||
OriginAccount: followRequest.Account, |
||||
TargetAccount: followRequest.TargetAccount, |
||||
}) |
||||
} |
||||
|
||||
return p.RelationshipGet(ctx, requestingAccount, sourceAccountID) |
||||
} |
||||
|
||||
// FollowRequestsGet fetches a list of the accounts that are follow requesting the given requestingAccount (the currently authorized account).
|
||||
func (p *Processor) FollowRequestsGet(ctx context.Context, requestingAccount *gtsmodel.Account, page *paging.Page) (*apimodel.PageableResponse, gtserror.WithCode) { |
||||
// Fetch follow requests targeting the given requesting account model.
|
||||
followRequests, err := p.state.DB.GetAccountFollowRequests(ctx, requestingAccount.ID, page) |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
// Check for empty response.
|
||||
count := len(followRequests) |
||||
if count == 0 { |
||||
return paging.EmptyResponse(), nil |
||||
} |
||||
|
||||
// Get the lowest and highest
|
||||
// ID values, used for paging.
|
||||
lo := followRequests[count-1].ID |
||||
hi := followRequests[0].ID |
||||
|
||||
// Func to fetch follow source at index.
|
||||
getIdx := func(i int) *gtsmodel.Account { |
||||
return followRequests[i].Account |
||||
} |
||||
|
||||
// Get a filtered slice of public API account models.
|
||||
items := p.c.GetVisibleAPIAccountsPaged(ctx, |
||||
requestingAccount, |
||||
getIdx, |
||||
count, |
||||
) |
||||
|
||||
return paging.PackageResponse(paging.ResponseParams{ |
||||
Items: items, |
||||
Path: "/api/v1/follow_requests", |
||||
Next: page.Next(lo, hi), |
||||
Prev: page.Prev(lo, hi), |
||||
}), nil |
||||
} |
||||
@ -1,86 +0,0 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package processing |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
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/log" |
||||
"github.com/superseriousbusiness/gotosocial/internal/paging" |
||||
"github.com/superseriousbusiness/gotosocial/internal/util" |
||||
) |
||||
|
||||
// BlocksGet ...
|
||||
func (p *Processor) BlocksGet( |
||||
ctx context.Context, |
||||
requestingAccount *gtsmodel.Account, |
||||
page *paging.Page, |
||||
) (*apimodel.PageableResponse, gtserror.WithCode) { |
||||
blocks, err := p.state.DB.GetAccountBlocks(ctx, |
||||
requestingAccount.ID, |
||||
page, |
||||
) |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
// Check for zero length.
|
||||
count := len(blocks) |
||||
if len(blocks) == 0 { |
||||
return util.EmptyPageableResponse(), nil |
||||
} |
||||
|
||||
var ( |
||||
items = make([]interface{}, 0, count) |
||||
|
||||
// Set next + prev values before API converting
|
||||
// so the caller can still page even on error.
|
||||
nextMaxIDValue = blocks[count-1].ID |
||||
prevMinIDValue = blocks[0].ID |
||||
) |
||||
|
||||
for _, block := range blocks { |
||||
if block.TargetAccount == nil { |
||||
// All models should be populated at this point.
|
||||
log.Warnf(ctx, "block target account was nil: %v", err) |
||||
continue |
||||
} |
||||
|
||||
// Convert target account to frontend API model.
|
||||
account, err := p.tc.AccountToAPIAccountBlocked(ctx, block.TargetAccount) |
||||
if err != nil { |
||||
log.Errorf(ctx, "error converting account to public api account: %v", err) |
||||
continue |
||||
} |
||||
|
||||
// Append target to return items.
|
||||
items = append(items, account) |
||||
} |
||||
|
||||
return paging.PackageResponse(paging.ResponseParams{ |
||||
Items: items, |
||||
Path: "/api/v1/blocks", |
||||
Next: page.Next(nextMaxIDValue), |
||||
Prev: page.Prev(prevMinIDValue), |
||||
}), nil |
||||
} |
||||
@ -0,0 +1,238 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package common |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
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/log" |
||||
) |
||||
|
||||
// GetTargetAccountBy fetches the target account with db load function, given the authorized (or, nil) requester's
|
||||
// account. This returns an approprate gtserror.WithCode accounting (ha) for not found and visibility to requester.
|
||||
func (p *Processor) GetTargetAccountBy( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
getTargetFromDB func() (*gtsmodel.Account, error), |
||||
) ( |
||||
account *gtsmodel.Account, |
||||
visible bool, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
// Fetch the target account from db.
|
||||
target, err := getTargetFromDB() |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return nil, false, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
if target == nil { |
||||
// DB loader could not find account in database.
|
||||
err := errors.New("target account not found") |
||||
return nil, false, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
// Check whether target account is visible to requesting account.
|
||||
visible, err = p.filter.AccountVisible(ctx, requester, target) |
||||
if err != nil { |
||||
return nil, false, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
if requester != nil && visible { |
||||
// Ensure the account is up-to-date.
|
||||
p.federator.RefreshAccountAsync(ctx, |
||||
requester.Username, |
||||
target, |
||||
nil, |
||||
false, |
||||
) |
||||
} |
||||
|
||||
return target, visible, nil |
||||
} |
||||
|
||||
// GetTargetAccountByID is a call-through to GetTargetAccountBy() using the db GetAccountByID() function.
|
||||
func (p *Processor) GetTargetAccountByID( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
targetID string, |
||||
) ( |
||||
account *gtsmodel.Account, |
||||
visible bool, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
return p.GetTargetAccountBy(ctx, requester, func() (*gtsmodel.Account, error) { |
||||
return p.state.DB.GetAccountByID(ctx, targetID) |
||||
}) |
||||
} |
||||
|
||||
// GetVisibleTargetAccount calls GetTargetAccountByID(),
|
||||
// but converts a non-visible result to not-found error.
|
||||
func (p *Processor) GetVisibleTargetAccount( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
targetID string, |
||||
) ( |
||||
account *gtsmodel.Account, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
// Fetch the target account by ID from the database.
|
||||
target, visible, errWithCode := p.GetTargetAccountByID(ctx, |
||||
requester, |
||||
targetID, |
||||
) |
||||
if errWithCode != nil { |
||||
return nil, errWithCode |
||||
} |
||||
|
||||
if !visible { |
||||
// Pretend account doesn't exist if not visible.
|
||||
err := errors.New("target account not found") |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
return target, nil |
||||
} |
||||
|
||||
// GetAPIAccount fetches the appropriate API account model depending on whether requester = target.
|
||||
func (p *Processor) GetAPIAccount( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
target *gtsmodel.Account, |
||||
) ( |
||||
apiAcc *apimodel.Account, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
var err error |
||||
|
||||
if requester != nil && requester.ID == target.ID { |
||||
// Only return sensitive account model _if_ requester = target.
|
||||
apiAcc, err = p.converter.AccountToAPIAccountSensitive(ctx, target) |
||||
} else { |
||||
// Else, fall back to returning the public account model.
|
||||
apiAcc, err = p.converter.AccountToAPIAccountPublic(ctx, target) |
||||
} |
||||
|
||||
if err != nil { |
||||
err := gtserror.Newf("error converting account: %w", err) |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
return apiAcc, nil |
||||
} |
||||
|
||||
// GetAPIAccountBlocked fetches the limited "blocked" account model for given target.
|
||||
func (p *Processor) GetAPIAccountBlocked( |
||||
ctx context.Context, |
||||
targetAcc *gtsmodel.Account, |
||||
) ( |
||||
apiAcc *apimodel.Account, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAcc) |
||||
if err != nil { |
||||
err = gtserror.Newf("error converting account: %w", err) |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
return apiAccount, nil |
||||
} |
||||
|
||||
// GetVisibleAPIAccounts converts an array of gtsmodel.Accounts (inputted by next function) into
|
||||
// public API model accounts, checking first for visibility. Please note that all errors will be
|
||||
// logged at ERROR level, but will not be returned. Callers are likely to run into show-stopping
|
||||
// errors in the lead-up to this function, whereas calling this should not be a show-stopper.
|
||||
func (p *Processor) GetVisibleAPIAccounts( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Account, |
||||
length int, |
||||
) []*apimodel.Account { |
||||
return p.getVisibleAPIAccounts(ctx, 3, requester, next, length) |
||||
} |
||||
|
||||
// GetVisibleAPIAccountsPaged is functionally equivalent to GetVisibleAPIAccounts(),
|
||||
// except the accounts are returned as a converted slice of accounts as interface{}.
|
||||
func (p *Processor) GetVisibleAPIAccountsPaged( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Account, |
||||
length int, |
||||
) []interface{} { |
||||
accounts := p.getVisibleAPIAccounts(ctx, 3, requester, next, length) |
||||
if len(accounts) == 0 { |
||||
return nil |
||||
} |
||||
items := make([]interface{}, len(accounts)) |
||||
for i, account := range accounts { |
||||
items[i] = account |
||||
} |
||||
return items |
||||
} |
||||
|
||||
func (p *Processor) getVisibleAPIAccounts( |
||||
ctx context.Context, |
||||
calldepth int, // used to skip wrapping func above these's names
|
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Account, |
||||
length int, |
||||
) []*apimodel.Account { |
||||
// Start new log entry with
|
||||
// the above calling func's name.
|
||||
l := log. |
||||
WithContext(ctx). |
||||
WithField("caller", log.Caller(calldepth+1)) |
||||
|
||||
// Preallocate slice according to expected length.
|
||||
accounts := make([]*apimodel.Account, 0, length) |
||||
|
||||
for i := 0; i < length; i++ { |
||||
// Get next account.
|
||||
account := next(i) |
||||
if account == nil { |
||||
continue |
||||
} |
||||
|
||||
// Check whether this account is visible to requesting account.
|
||||
visible, err := p.filter.AccountVisible(ctx, requester, account) |
||||
if err != nil { |
||||
l.Errorf("error checking account visibility: %v", err) |
||||
continue |
||||
} |
||||
|
||||
if !visible { |
||||
// Not visible to requester.
|
||||
continue |
||||
} |
||||
|
||||
// Convert the account to a public API model representation.
|
||||
apiAcc, err := p.converter.AccountToAPIAccountPublic(ctx, account) |
||||
if err != nil { |
||||
l.Errorf("error converting account: %v", err) |
||||
continue |
||||
} |
||||
|
||||
// Append API model to return slice.
|
||||
accounts = append(accounts, apiAcc) |
||||
} |
||||
|
||||
return accounts |
||||
} |
||||
@ -0,0 +1,50 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package common |
||||
|
||||
import ( |
||||
"github.com/superseriousbusiness/gotosocial/internal/federation" |
||||
"github.com/superseriousbusiness/gotosocial/internal/state" |
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils" |
||||
"github.com/superseriousbusiness/gotosocial/internal/visibility" |
||||
) |
||||
|
||||
// Processor provides a processor with logic
|
||||
// common to multiple logical domains of the
|
||||
// processing subsection of the codebase.
|
||||
type Processor struct { |
||||
state *state.State |
||||
converter typeutils.TypeConverter |
||||
federator federation.Federator |
||||
filter *visibility.Filter |
||||
} |
||||
|
||||
// New returns a new Processor instance.
|
||||
func New( |
||||
state *state.State, |
||||
converter typeutils.TypeConverter, |
||||
federator federation.Federator, |
||||
filter *visibility.Filter, |
||||
) Processor { |
||||
return Processor{ |
||||
state: state, |
||||
converter: converter, |
||||
federator: federator, |
||||
filter: filter, |
||||
} |
||||
} |
||||
@ -0,0 +1,248 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package common |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
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/log" |
||||
) |
||||
|
||||
// GetTargetStatusBy fetches the target status with db load function, given the authorized (or, nil) requester's
|
||||
// account. This returns an approprate gtserror.WithCode accounting for not found and visibility to requester.
|
||||
func (p *Processor) GetTargetStatusBy( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
getTargetFromDB func() (*gtsmodel.Status, error), |
||||
) ( |
||||
status *gtsmodel.Status, |
||||
visible bool, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
// Fetch the target status from db.
|
||||
target, err := getTargetFromDB() |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return nil, false, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
if target == nil { |
||||
// DB loader could not find status in database.
|
||||
err := errors.New("target status not found") |
||||
return nil, false, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
// Check whether target status is visible to requesting account.
|
||||
visible, err = p.filter.StatusVisible(ctx, requester, target) |
||||
if err != nil { |
||||
return nil, false, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
if requester != nil && visible { |
||||
// Ensure remote status is up-to-date.
|
||||
p.federator.RefreshStatusAsync(ctx, |
||||
requester.Username, |
||||
target, |
||||
nil, |
||||
false, |
||||
) |
||||
} |
||||
|
||||
return target, visible, nil |
||||
} |
||||
|
||||
// GetTargetStatusByID is a call-through to GetTargetStatus() using the db GetStatusByID() function.
|
||||
func (p *Processor) GetTargetStatusByID( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
targetID string, |
||||
) ( |
||||
status *gtsmodel.Status, |
||||
visible bool, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
return p.GetTargetStatusBy(ctx, requester, func() (*gtsmodel.Status, error) { |
||||
return p.state.DB.GetStatusByID(ctx, targetID) |
||||
}) |
||||
} |
||||
|
||||
// GetVisibleTargetStatus calls GetTargetStatusByID(),
|
||||
// but converts a non-visible result to not-found error.
|
||||
func (p *Processor) GetVisibleTargetStatus( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
targetID string, |
||||
) ( |
||||
status *gtsmodel.Status, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
// Fetch the target status by ID from the database.
|
||||
target, visible, errWithCode := p.GetTargetStatusByID(ctx, |
||||
requester, |
||||
targetID, |
||||
) |
||||
if errWithCode != nil { |
||||
return nil, errWithCode |
||||
} |
||||
|
||||
if !visible { |
||||
// Target should not be seen by requester.
|
||||
err := errors.New("target status not found") |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
return target, nil |
||||
} |
||||
|
||||
// GetAPIStatus fetches the appropriate API status model for target.
|
||||
func (p *Processor) GetAPIStatus( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
target *gtsmodel.Status, |
||||
) ( |
||||
apiStatus *apimodel.Status, |
||||
errWithCode gtserror.WithCode, |
||||
) { |
||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester) |
||||
if err != nil { |
||||
err = gtserror.Newf("error converting status: %w", err) |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
return apiStatus, nil |
||||
} |
||||
|
||||
// GetVisibleAPIStatuses converts an array of gtsmodel.Status (inputted by next function) into
|
||||
// API model statuses, checking first for visibility. Please note that all errors will be
|
||||
// logged at ERROR level, but will not be returned. Callers are likely to run into show-stopping
|
||||
// errors in the lead-up to this function, whereas calling this should not be a show-stopper.
|
||||
func (p *Processor) GetVisibleAPIStatuses( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Status, |
||||
length int, |
||||
) []*apimodel.Status { |
||||
return p.getVisibleAPIStatuses(ctx, 3, requester, next, length) |
||||
} |
||||
|
||||
// GetVisibleAPIStatusesPaged is functionally equivalent to GetVisibleAPIStatuses(),
|
||||
// except the statuses are returned as a converted slice of statuses as interface{}.
|
||||
func (p *Processor) GetVisibleAPIStatusesPaged( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Status, |
||||
length int, |
||||
) []interface{} { |
||||
statuses := p.getVisibleAPIStatuses(ctx, 3, requester, next, length) |
||||
if len(statuses) == 0 { |
||||
return nil |
||||
} |
||||
items := make([]interface{}, len(statuses)) |
||||
for i, status := range statuses { |
||||
items[i] = status |
||||
} |
||||
return items |
||||
} |
||||
|
||||
func (p *Processor) getVisibleAPIStatuses( |
||||
ctx context.Context, |
||||
calldepth int, // used to skip wrapping func above these's names
|
||||
requester *gtsmodel.Account, |
||||
next func(int) *gtsmodel.Status, |
||||
length int, |
||||
) []*apimodel.Status { |
||||
// Start new log entry with
|
||||
// the above calling func's name.
|
||||
l := log. |
||||
WithContext(ctx). |
||||
WithField("caller", log.Caller(calldepth+1)) |
||||
|
||||
// Preallocate slice according to expected length.
|
||||
statuses := make([]*apimodel.Status, 0, length) |
||||
|
||||
for i := 0; i < length; i++ { |
||||
// Get next status.
|
||||
status := next(i) |
||||
if status == nil { |
||||
continue |
||||
} |
||||
|
||||
// Check whether this status is visible to requesting account.
|
||||
visible, err := p.filter.StatusVisible(ctx, requester, status) |
||||
if err != nil { |
||||
l.Errorf("error checking status visibility: %v", err) |
||||
continue |
||||
} |
||||
|
||||
if !visible { |
||||
// Not visible to requester.
|
||||
continue |
||||
} |
||||
|
||||
// Convert the status to an API model representation.
|
||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester) |
||||
if err != nil { |
||||
l.Errorf("error converting status: %v", err) |
||||
continue |
||||
} |
||||
|
||||
// Append API model to return slice.
|
||||
statuses = append(statuses, apiStatus) |
||||
} |
||||
|
||||
return statuses |
||||
} |
||||
|
||||
// InvalidateTimelinedStatus is a shortcut function for invalidating the cached
|
||||
// representation one status in the home timeline and all list timelines of the
|
||||
// given accountID. It should only be called in cases where a status update
|
||||
// does *not* need to be passed into the processor via the worker queue, since
|
||||
// such invalidation will, in that case, be handled by the processor instead.
|
||||
func (p *Processor) InvalidateTimelinedStatus(ctx context.Context, accountID string, statusID string) error { |
||||
// Get lists first + bail if this fails.
|
||||
lists, err := p.state.DB.GetListsForAccountID(ctx, accountID) |
||||
if err != nil { |
||||
return gtserror.Newf("db error getting lists for account %s: %w", accountID, err) |
||||
} |
||||
|
||||
// Start new log entry with
|
||||
// the above calling func's name.
|
||||
l := log. |
||||
WithContext(ctx). |
||||
WithField("caller", log.Caller(3)). |
||||
WithField("accountID", accountID). |
||||
WithField("statusID", statusID) |
||||
|
||||
// Unprepare item from home + list timelines, just log
|
||||
// if something goes wrong since this is not a showstopper.
|
||||
|
||||
if err := p.state.Timelines.Home.UnprepareItem(ctx, accountID, statusID); err != nil { |
||||
l.Errorf("error unpreparing item from home timeline: %v", err) |
||||
} |
||||
|
||||
for _, list := range lists { |
||||
if err := p.state.Timelines.List.UnprepareItem(ctx, list.ID, statusID); err != nil { |
||||
l.Errorf("error unpreparing item from list timeline %s: %v", list.ID, err) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
@ -1,123 +0,0 @@
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package processing |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap" |
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" |
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
||||
"github.com/superseriousbusiness/gotosocial/internal/log" |
||||
"github.com/superseriousbusiness/gotosocial/internal/messages" |
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth" |
||||
) |
||||
|
||||
func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { |
||||
followRequests, err := p.state.DB.GetAccountFollowRequests(ctx, auth.Account.ID) |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
accts := make([]apimodel.Account, 0, len(followRequests)) |
||||
for _, followRequest := range followRequests { |
||||
if followRequest.Account == nil { |
||||
// The creator of the follow doesn't exist,
|
||||
// just skip this one.
|
||||
log.WithContext(ctx).WithField("followRequest", followRequest).Warn("follow request had no associated account") |
||||
continue |
||||
} |
||||
|
||||
apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, followRequest.Account) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
accts = append(accts, *apiAcct) |
||||
} |
||||
|
||||
return accts, nil |
||||
} |
||||
|
||||
func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { |
||||
follow, err := p.state.DB.AcceptFollowRequest(ctx, accountID, auth.Account.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
if follow.Account == nil { |
||||
// The creator of the follow doesn't exist,
|
||||
// so we can't do further processing.
|
||||
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no associated account") |
||||
return p.relationship(ctx, auth.Account.ID, accountID) |
||||
} |
||||
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ |
||||
APObjectType: ap.ActivityFollow, |
||||
APActivityType: ap.ActivityAccept, |
||||
GTSModel: follow, |
||||
OriginAccount: follow.Account, |
||||
TargetAccount: follow.TargetAccount, |
||||
}) |
||||
|
||||
return p.relationship(ctx, auth.Account.ID, accountID) |
||||
} |
||||
|
||||
func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { |
||||
followRequest, err := p.state.DB.GetFollowRequest(ctx, accountID, auth.Account.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
err = p.state.DB.RejectFollowRequest(ctx, accountID, auth.Account.ID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorNotFound(err) |
||||
} |
||||
|
||||
if followRequest.Account == nil { |
||||
// The creator of the request doesn't exist,
|
||||
// so we can't do further processing.
|
||||
return p.relationship(ctx, auth.Account.ID, accountID) |
||||
} |
||||
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ |
||||
APObjectType: ap.ActivityFollow, |
||||
APActivityType: ap.ActivityReject, |
||||
GTSModel: followRequest, |
||||
OriginAccount: followRequest.Account, |
||||
TargetAccount: followRequest.TargetAccount, |
||||
}) |
||||
|
||||
return p.relationship(ctx, auth.Account.ID, accountID) |
||||
} |
||||
|
||||
func (p *Processor) relationship(ctx context.Context, accountID string, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { |
||||
relationship, err := p.state.DB.GetRelationship(ctx, accountID, targetAccountID) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
apiRelationship, err := p.tc.RelationshipToAPIRelationship(ctx, relationship) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
|
||||
return apiRelationship, nil |
||||
} |
||||
@ -0,0 +1,2 @@
|
||||
cpu.out |
||||
linkheader.test |
||||
@ -0,0 +1,6 @@
|
||||
language: go |
||||
|
||||
go: |
||||
- 1.6 |
||||
- 1.7 |
||||
- tip |
||||
@ -0,0 +1,10 @@
|
||||
# Contributing |
||||
|
||||
* Raise an issue if appropriate |
||||
* Fork the repo |
||||
* Bootstrap the dev dependencies (run `./script/bootstrap`) |
||||
* Make your changes |
||||
* Use [gofmt](https://golang.org/cmd/gofmt/) |
||||
* Make sure the tests pass (run `./script/test`) |
||||
* Make sure the linters pass (run `./script/lint`) |
||||
* Issue a pull request |
||||
@ -0,0 +1,21 @@
|
||||
MIT License |
||||
|
||||
Copyright (c) 2016 Tom Hudson |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
||||
@ -0,0 +1,35 @@
|
||||
# Golang Link Header Parser |
||||
|
||||
Library for parsing HTTP Link headers. Requires Go 1.6 or higher. |
||||
|
||||
Docs can be found on [the GoDoc page](https://godoc.org/github.com/tomnomnom/linkheader). |
||||
|
||||
[](https://travis-ci.org/tomnomnom/linkheader) |
||||
|
||||
## Basic Example |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/tomnomnom/linkheader" |
||||
) |
||||
|
||||
func main() { |
||||
header := "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"," + |
||||
"<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"" |
||||
links := linkheader.Parse(header) |
||||
|
||||
for _, link := range links { |
||||
fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) |
||||
} |
||||
} |
||||
|
||||
// Output: |
||||
// URL: https://api.github.com/user/58276/repos?page=2; Rel: next |
||||
// URL: https://api.github.com/user/58276/repos?page=2; Rel: last |
||||
``` |
||||
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
// Package linkheader provides functions for parsing HTTP Link headers
|
||||
package linkheader |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
// A Link is a single URL and related parameters
|
||||
type Link struct { |
||||
URL string |
||||
Rel string |
||||
Params map[string]string |
||||
} |
||||
|
||||
// HasParam returns if a Link has a particular parameter or not
|
||||
func (l Link) HasParam(key string) bool { |
||||
for p := range l.Params { |
||||
if p == key { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// Param returns the value of a parameter if it exists
|
||||
func (l Link) Param(key string) string { |
||||
for k, v := range l.Params { |
||||
if key == k { |
||||
return v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
// String returns the string representation of a link
|
||||
func (l Link) String() string { |
||||
|
||||
p := make([]string, 0, len(l.Params)) |
||||
for k, v := range l.Params { |
||||
p = append(p, fmt.Sprintf("%s=\"%s\"", k, v)) |
||||
} |
||||
if l.Rel != "" { |
||||
p = append(p, fmt.Sprintf("%s=\"%s\"", "rel", l.Rel)) |
||||
} |
||||
return fmt.Sprintf("<%s>; %s", l.URL, strings.Join(p, "; ")) |
||||
} |
||||
|
||||
// Links is a slice of Link structs
|
||||
type Links []Link |
||||
|
||||
// FilterByRel filters a group of Links by the provided Rel attribute
|
||||
func (l Links) FilterByRel(r string) Links { |
||||
links := make(Links, 0) |
||||
for _, link := range l { |
||||
if link.Rel == r { |
||||
links = append(links, link) |
||||
} |
||||
} |
||||
return links |
||||
} |
||||
|
||||
// String returns the string representation of multiple Links
|
||||
// for use in HTTP responses etc
|
||||
func (l Links) String() string { |
||||
if l == nil { |
||||
return fmt.Sprint(nil) |
||||
} |
||||
|
||||
var strs []string |
||||
for _, link := range l { |
||||
strs = append(strs, link.String()) |
||||
} |
||||
return strings.Join(strs, ", ") |
||||
} |
||||
|
||||
// Parse parses a raw Link header in the form:
|
||||
// <url>; rel="foo", <url>; rel="bar"; wat="dis"
|
||||
// returning a slice of Link structs
|
||||
func Parse(raw string) Links { |
||||
var links Links |
||||
|
||||
// One chunk: <url>; rel="foo"
|
||||
for _, chunk := range strings.Split(raw, ",") { |
||||
|
||||
link := Link{URL: "", Rel: "", Params: make(map[string]string)} |
||||
|
||||
// Figure out what each piece of the chunk is
|
||||
for _, piece := range strings.Split(chunk, ";") { |
||||
|
||||
piece = strings.Trim(piece, " ") |
||||
if piece == "" { |
||||
continue |
||||
} |
||||
|
||||
// URL
|
||||
if piece[0] == '<' && piece[len(piece)-1] == '>' { |
||||
link.URL = strings.Trim(piece, "<>") |
||||
continue |
||||
} |
||||
|
||||
// Params
|
||||
key, val := parseParam(piece) |
||||
if key == "" { |
||||
continue |
||||
} |
||||
|
||||
// Special case for rel
|
||||
if strings.ToLower(key) == "rel" { |
||||
link.Rel = val |
||||
} else { |
||||
link.Params[key] = val |
||||
} |
||||
} |
||||
|
||||
if link.URL != "" { |
||||
links = append(links, link) |
||||
} |
||||
} |
||||
|
||||
return links |
||||
} |
||||
|
||||
// ParseMultiple is like Parse, but accepts a slice of headers
|
||||
// rather than just one header string
|
||||
func ParseMultiple(headers []string) Links { |
||||
links := make(Links, 0) |
||||
for _, header := range headers { |
||||
links = append(links, Parse(header)...) |
||||
} |
||||
return links |
||||
} |
||||
|
||||
// parseParam takes a raw param in the form key="val" and
|
||||
// returns the key and value as seperate strings
|
||||
func parseParam(raw string) (key, val string) { |
||||
|
||||
parts := strings.SplitN(raw, "=", 2) |
||||
if len(parts) == 1 { |
||||
return parts[0], "" |
||||
} |
||||
if len(parts) != 2 { |
||||
return "", "" |
||||
} |
||||
|
||||
key = parts[0] |
||||
val = strings.Trim(parts[1], "\"") |
||||
|
||||
return key, val |
||||
|
||||
} |
||||
Loading…
Reference in new issue