mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Web profile pages for accounts (#449)
* add default avatars * allow webModule to error * return errWithCode from account get * add AccountGetLocalByUsername * check nil requesting account * add timestampShort function for just month/year * move loading logic to New + add default avatars * add profile page view * update swagger docs * add excludeReblogs to GetAccountStatuses * ignore casing when selecting local account by username * appropriate redirects * css fiddling * add 'about' heading * adjust thread page to work with routing * return AP representation if requested + authorized * simplify auth check * go fmt * golangci-lint ignore math/rand
This commit is contained in:
@@ -34,16 +34,20 @@ func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth,
|
||||
return p.accountProcessor.DeleteLocal(ctx, authed.Account, form)
|
||||
}
|
||||
|
||||
func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
|
||||
func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
|
||||
return p.accountProcessor.Get(ctx, authed.Account, targetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) {
|
||||
return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username)
|
||||
}
|
||||
|
||||
func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
|
||||
return p.accountProcessor.Update(ctx, authed.Account, form)
|
||||
}
|
||||
|
||||
func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||
|
@@ -47,12 +47,14 @@ type Processor interface {
|
||||
// Unlike Delete, it will propagate the deletion out across the federating API to other instances.
|
||||
DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode
|
||||
// Get processes the given request for account information.
|
||||
Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)
|
||||
Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
|
||||
// GetLocalByUsername processes the given request for account information targeting a local account by username.
|
||||
GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode)
|
||||
// Update processes the update of an account with the given form
|
||||
Update(ctx context.Context, 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(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||
StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||
// FollowersGet fetches a list of the target account's followers.
|
||||
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||
// FollowingGet fetches a list of the accounts that target account is following.
|
||||
|
@@ -143,7 +143,7 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
|
||||
var maxID string
|
||||
selectStatusesLoop:
|
||||
for {
|
||||
statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, "", false, false, false)
|
||||
statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, false, maxID, "", false, false, false)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
// no statuses left for this instance so we're done
|
||||
|
@@ -26,23 +26,41 @@ 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) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) {
|
||||
func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
|
||||
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, errors.New("account not found")
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
|
||||
}
|
||||
return nil, fmt.Errorf("db error: %s", err)
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
|
||||
}
|
||||
|
||||
return p.getAccountFor(ctx, requestingAccount, targetAccount)
|
||||
}
|
||||
|
||||
func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) {
|
||||
targetAccount, err := p.db.GetLocalAccountByUsername(ctx, username)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
|
||||
}
|
||||
|
||||
return p.getAccountFor(ctx, requestingAccount, targetAccount)
|
||||
}
|
||||
|
||||
func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) {
|
||||
var blocked bool
|
||||
var err error
|
||||
if requestingAccount != nil {
|
||||
blocked, err = p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true)
|
||||
blocked, err = p.db.IsBlocked(ctx, requestingAccount.ID, targetAccount.ID, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking account block: %s", err)
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking account block: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +68,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
||||
if blocked {
|
||||
apiAccount, err = p.tc.AccountToAPIAccountBlocked(ctx, targetAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting account: %s", err)
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err))
|
||||
}
|
||||
return apiAccount, nil
|
||||
}
|
||||
@@ -59,7 +77,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
||||
if targetAccount.Domain != "" {
|
||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing url %s: %s", targetAccount.URI, err)
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", targetAccount.URI, err))
|
||||
}
|
||||
|
||||
a, err := p.federator.GetRemoteAccount(ctx, requestingAccount.Username, targetAccountURI, true, false)
|
||||
@@ -74,7 +92,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
||||
apiAccount, err = p.tc.AccountToAPIAccountPublic(ctx, targetAccount)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting account: %s", err)
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err))
|
||||
}
|
||||
return apiAccount, nil
|
||||
}
|
||||
|
@@ -28,16 +28,18 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
} else if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||
if requestingAccount != nil {
|
||||
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
} else if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
}
|
||||
|
||||
apiStatuses := []apimodel.Status{}
|
||||
|
||||
statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return apiStatuses, nil
|
||||
|
@@ -89,7 +89,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag
|
||||
|
||||
// scenario 2 -- get the requested page
|
||||
// limit pages to 30 entries per page
|
||||
publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, maxID, minID, false, false, true)
|
||||
publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true)
|
||||
if err != nil && err != db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
@@ -38,17 +38,20 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
|
||||
}
|
||||
|
||||
var requestedPerson vocab.ActivityStreamsPerson
|
||||
switch {
|
||||
case uris.IsPublicKeyPath(requestURL):
|
||||
if uris.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(ctx, requestedAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
case uris.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
|
||||
} else {
|
||||
// if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
|
||||
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||
if err != nil || !authenticated {
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotAuthorized(err, "not authorized")
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
|
||||
}
|
||||
|
||||
@@ -73,8 +76,6 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
default:
|
||||
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
|
||||
}
|
||||
|
||||
data, err := streams.Serialize(requestedPerson)
|
||||
|
@@ -354,7 +354,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
||||
suite.False(zorkFollowsSatan)
|
||||
|
||||
// no statuses from foss satan should be left in the database
|
||||
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false)
|
||||
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Empty(dbStatuses)
|
||||
|
||||
|
@@ -76,12 +76,14 @@ type Processor interface {
|
||||
// AccountDeleteLocal processes the delete of a LOCAL account using the given form.
|
||||
AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode
|
||||
// AccountGet processes the given request for account information.
|
||||
AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error)
|
||||
AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
|
||||
// AccountGet processes the given request for account information.
|
||||
AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode)
|
||||
// AccountUpdate processes the update of an account with the given form
|
||||
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
|
||||
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||
// the account given in authed.
|
||||
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||
// AccountFollowersGet fetches a list of the target account's followers.
|
||||
AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||
// AccountFollowingGet fetches a list of the accounts that target account is following.
|
||||
|
Reference in New Issue
Block a user