mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 19:01:01 +00:00
[feature/frontend] Allow setting alt-text for avatar + header (#3086)
This commit is contained in:
parent
43c480aec4
commit
d70f4e166d
18 changed files with 395 additions and 140 deletions
|
@ -193,6 +193,11 @@ definitions:
|
||||||
example: https://example.org/media/some_user/avatar/original/avatar.jpeg
|
example: https://example.org/media/some_user/avatar/original/avatar.jpeg
|
||||||
type: string
|
type: string
|
||||||
x-go-name: Avatar
|
x-go-name: Avatar
|
||||||
|
avatar_description:
|
||||||
|
description: Description of this account's avatar, for alt text.
|
||||||
|
example: A cute drawing of a smiling sloth.
|
||||||
|
type: string
|
||||||
|
x-go-name: AvatarDescription
|
||||||
avatar_static:
|
avatar_static:
|
||||||
description: |-
|
description: |-
|
||||||
Web location of a static version of the account's avatar.
|
Web location of a static version of the account's avatar.
|
||||||
|
@ -259,6 +264,11 @@ definitions:
|
||||||
example: https://example.org/media/some_user/header/original/header.jpeg
|
example: https://example.org/media/some_user/header/original/header.jpeg
|
||||||
type: string
|
type: string
|
||||||
x-go-name: Header
|
x-go-name: Header
|
||||||
|
header_description:
|
||||||
|
description: Description of this account's header, for alt text.
|
||||||
|
example: A sunlit field with purple flowers.
|
||||||
|
type: string
|
||||||
|
x-go-name: HeaderDescription
|
||||||
header_static:
|
header_static:
|
||||||
description: |-
|
description: |-
|
||||||
Web location of a static version of the account's header.
|
Web location of a static version of the account's header.
|
||||||
|
@ -1948,6 +1958,11 @@ definitions:
|
||||||
example: https://example.org/media/some_user/avatar/original/avatar.jpeg
|
example: https://example.org/media/some_user/avatar/original/avatar.jpeg
|
||||||
type: string
|
type: string
|
||||||
x-go-name: Avatar
|
x-go-name: Avatar
|
||||||
|
avatar_description:
|
||||||
|
description: Description of this account's avatar, for alt text.
|
||||||
|
example: A cute drawing of a smiling sloth.
|
||||||
|
type: string
|
||||||
|
x-go-name: AvatarDescription
|
||||||
avatar_static:
|
avatar_static:
|
||||||
description: |-
|
description: |-
|
||||||
Web location of a static version of the account's avatar.
|
Web location of a static version of the account's avatar.
|
||||||
|
@ -2014,6 +2029,11 @@ definitions:
|
||||||
example: https://example.org/media/some_user/header/original/header.jpeg
|
example: https://example.org/media/some_user/header/original/header.jpeg
|
||||||
type: string
|
type: string
|
||||||
x-go-name: Header
|
x-go-name: Header
|
||||||
|
header_description:
|
||||||
|
description: Description of this account's header, for alt text.
|
||||||
|
example: A sunlit field with purple flowers.
|
||||||
|
type: string
|
||||||
|
x-go-name: HeaderDescription
|
||||||
header_static:
|
header_static:
|
||||||
description: |-
|
description: |-
|
||||||
Web location of a static version of the account's header.
|
Web location of a static version of the account's header.
|
||||||
|
@ -4072,10 +4092,20 @@ paths:
|
||||||
in: formData
|
in: formData
|
||||||
name: avatar
|
name: avatar
|
||||||
type: file
|
type: file
|
||||||
|
- allowEmptyValue: true
|
||||||
|
description: Description of avatar image, for alt-text.
|
||||||
|
in: formData
|
||||||
|
name: avatar_description
|
||||||
|
type: string
|
||||||
- description: Header of the user.
|
- description: Header of the user.
|
||||||
in: formData
|
in: formData
|
||||||
name: header
|
name: header
|
||||||
type: file
|
type: file
|
||||||
|
- allowEmptyValue: true
|
||||||
|
description: Description of header image, for alt-text.
|
||||||
|
in: formData
|
||||||
|
name: header_description
|
||||||
|
type: string
|
||||||
- description: Require manual approval of follow requests.
|
- description: Require manual approval of follow requests.
|
||||||
in: formData
|
in: formData
|
||||||
name: locked
|
name: locked
|
||||||
|
|
|
@ -78,11 +78,23 @@ import (
|
||||||
// description: Avatar of the user.
|
// description: Avatar of the user.
|
||||||
// type: file
|
// type: file
|
||||||
// -
|
// -
|
||||||
|
// name: avatar_description
|
||||||
|
// in: formData
|
||||||
|
// description: Description of avatar image, for alt-text.
|
||||||
|
// type: string
|
||||||
|
// allowEmptyValue: true
|
||||||
|
// -
|
||||||
// name: header
|
// name: header
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: Header of the user.
|
// description: Header of the user.
|
||||||
// type: file
|
// type: file
|
||||||
// -
|
// -
|
||||||
|
// name: header_description
|
||||||
|
// in: formData
|
||||||
|
// description: Description of header image, for alt-text.
|
||||||
|
// type: string
|
||||||
|
// allowEmptyValue: true
|
||||||
|
// -
|
||||||
// name: locked
|
// name: locked
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: Require manual approval of follow requests.
|
// description: Require manual approval of follow requests.
|
||||||
|
@ -315,7 +327,9 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
|
||||||
form.DisplayName == nil &&
|
form.DisplayName == nil &&
|
||||||
form.Note == nil &&
|
form.Note == nil &&
|
||||||
form.Avatar == nil &&
|
form.Avatar == nil &&
|
||||||
|
form.AvatarDescription == nil &&
|
||||||
form.Header == nil &&
|
form.Header == nil &&
|
||||||
|
form.HeaderDescription == nil &&
|
||||||
form.Locked == nil &&
|
form.Locked == nil &&
|
||||||
form.Source.Privacy == nil &&
|
form.Source.Privacy == nil &&
|
||||||
form.Source.Sensitive == nil &&
|
form.Source.Sensitive == nil &&
|
||||||
|
|
|
@ -234,8 +234,10 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -409,6 +411,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
||||||
"avatar_static": "",
|
"avatar_static": "",
|
||||||
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||||
|
"header_description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||||
"followers_count": 0,
|
"followers_count": 0,
|
||||||
"following_count": 0,
|
"following_count": 0,
|
||||||
"statuses_count": 0,
|
"statuses_count": 0,
|
||||||
|
|
|
@ -108,8 +108,10 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
|
|
@ -126,8 +126,10 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -189,8 +191,10 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
|
|
@ -62,6 +62,9 @@ type Account struct {
|
||||||
// Only relevant when the account's main avatar is a video or a gif.
|
// Only relevant when the account's main avatar is a video or a gif.
|
||||||
// example: https://example.org/media/some_user/avatar/static/avatar.png
|
// example: https://example.org/media/some_user/avatar/static/avatar.png
|
||||||
AvatarStatic string `json:"avatar_static"`
|
AvatarStatic string `json:"avatar_static"`
|
||||||
|
// Description of this account's avatar, for alt text.
|
||||||
|
// example: A cute drawing of a smiling sloth.
|
||||||
|
AvatarDescription string `json:"avatar_description,omitempty"`
|
||||||
// Web location of the account's header image.
|
// Web location of the account's header image.
|
||||||
// example: https://example.org/media/some_user/header/original/header.jpeg
|
// example: https://example.org/media/some_user/header/original/header.jpeg
|
||||||
Header string `json:"header"`
|
Header string `json:"header"`
|
||||||
|
@ -69,6 +72,9 @@ type Account struct {
|
||||||
// Only relevant when the account's main header is a video or a gif.
|
// Only relevant when the account's main header is a video or a gif.
|
||||||
// example: https://example.org/media/some_user/header/static/header.png
|
// example: https://example.org/media/some_user/header/static/header.png
|
||||||
HeaderStatic string `json:"header_static"`
|
HeaderStatic string `json:"header_static"`
|
||||||
|
// Description of this account's header, for alt text.
|
||||||
|
// example: A sunlit field with purple flowers.
|
||||||
|
HeaderDescription string `json:"header_description,omitempty"`
|
||||||
// Number of accounts following this account, according to our instance.
|
// Number of accounts following this account, according to our instance.
|
||||||
FollowersCount int `json:"followers_count"`
|
FollowersCount int `json:"followers_count"`
|
||||||
// Number of account's followed by this account, according to our instance.
|
// Number of account's followed by this account, according to our instance.
|
||||||
|
@ -104,6 +110,17 @@ type Account struct {
|
||||||
// If set, indicates that this account is currently inactive, and has migrated to the given account.
|
// If set, indicates that this account is currently inactive, and has migrated to the given account.
|
||||||
// Key/value omitted for accounts that haven't moved, and for suspended accounts.
|
// Key/value omitted for accounts that haven't moved, and for suspended accounts.
|
||||||
Moved *Account `json:"moved,omitempty"`
|
Moved *Account `json:"moved,omitempty"`
|
||||||
|
|
||||||
|
// Additional fields not exposed via JSON
|
||||||
|
// (used only internally for templating etc).
|
||||||
|
|
||||||
|
// Proper attachment model for the avatar.
|
||||||
|
//
|
||||||
|
// Only set if this model was converted via
|
||||||
|
// AccountToWebAccount, AND this account had
|
||||||
|
// an avatar set (and not just the default
|
||||||
|
// "blank" avatar image.)
|
||||||
|
AvatarAttachment *Attachment `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MutedAccount extends Account with a field used only by the muted user list.
|
// MutedAccount extends Account with a field used only by the muted user list.
|
||||||
|
@ -168,8 +185,12 @@ type UpdateCredentialsRequest struct {
|
||||||
Note *string `form:"note" json:"note"`
|
Note *string `form:"note" json:"note"`
|
||||||
// Avatar image encoded using multipart/form-data.
|
// Avatar image encoded using multipart/form-data.
|
||||||
Avatar *multipart.FileHeader `form:"avatar" json:"-"`
|
Avatar *multipart.FileHeader `form:"avatar" json:"-"`
|
||||||
|
// Description of the avatar image, for alt-text.
|
||||||
|
AvatarDescription *string `form:"avatar_description" json:"avatar_description"`
|
||||||
// Header image encoded using multipart/form-data
|
// Header image encoded using multipart/form-data
|
||||||
Header *multipart.FileHeader `form:"header" json:"-"`
|
Header *multipart.FileHeader `form:"header" json:"-"`
|
||||||
|
// Description of the header image, for alt-text.
|
||||||
|
HeaderDescription *string `form:"header_description" json:"header_description"`
|
||||||
// Require manual approval of follow requests.
|
// Require manual approval of follow requests.
|
||||||
Locked *bool `form:"locked" json:"locked"`
|
Locked *bool `form:"locked" json:"locked"`
|
||||||
// New Source values for this account.
|
// New Source values for this account.
|
||||||
|
|
|
@ -20,7 +20,6 @@ package account
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -36,66 +35,42 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
||||||
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
|
err := gtserror.New("account not found")
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
err := gtserror.Newf("db error getting account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.getFor(ctx, requestingAccount, targetAccount)
|
blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, targetAccount.ID)
|
||||||
}
|
|
||||||
|
|
||||||
// GetLocalByUsername processes the given request for account information targeting a local account by username.
|
|
||||||
func (p *Processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) {
|
|
||||||
targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
err := gtserror.Newf("db error checking blocks: %w", err)
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.getFor(ctx, requestingAccount, targetAccount)
|
if blocked {
|
||||||
}
|
apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAccount)
|
||||||
|
|
||||||
// GetCustomCSSForUsername returns custom css for the given local username.
|
|
||||||
func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) {
|
|
||||||
customCSS, err := p.state.DB.GetAccountCustomCSSByUsername(ctx, username)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
|
||||||
return "", gtserror.NewErrorNotFound(errors.New("account not found"))
|
|
||||||
}
|
|
||||||
return "", gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return customCSS, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if requestingAccount != nil {
|
|
||||||
blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, targetAccount.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking account block: %w", err))
|
err := gtserror.Newf("error converting account: %w", err)
|
||||||
}
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
|
||||||
if blocked {
|
|
||||||
apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %w", err))
|
|
||||||
}
|
|
||||||
return apiAccount, nil
|
|
||||||
}
|
}
|
||||||
|
return apiAccount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetAccount.Domain != "" {
|
if targetAccount.Domain != "" {
|
||||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %w", targetAccount.URI, err))
|
err := gtserror.Newf("error parsing account URI: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform a last-minute fetch of target account to ensure remote account header / avatar is cached.
|
// Perform a last-minute fetch of target account to
|
||||||
latest, _, err := p.federator.GetAccountByURI(gtscontext.SetFastFail(ctx), requestingAccount.Username, targetAccountURI)
|
// ensure remote account header / avatar is cached.
|
||||||
|
latest, _, err := p.federator.GetAccountByURI(
|
||||||
|
gtscontext.SetFastFail(ctx),
|
||||||
|
requestingAccount.Username,
|
||||||
|
targetAccountURI,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error fetching latest target account: %v", err)
|
log.Errorf(ctx, "error fetching latest target account: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,15 +80,53 @@ func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiAccount *apimodel.Account
|
var apiAccount *apimodel.Account
|
||||||
|
if targetAccount.ID == requestingAccount.ID {
|
||||||
if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
|
// This is requester's own account,
|
||||||
|
// show additional details.
|
||||||
apiAccount, err = p.converter.AccountToAPIAccountSensitive(ctx, targetAccount)
|
apiAccount, err = p.converter.AccountToAPIAccountSensitive(ctx, targetAccount)
|
||||||
} else {
|
} else {
|
||||||
|
// This is a different account,
|
||||||
|
// show the "public" view.
|
||||||
apiAccount, err = p.converter.AccountToAPIAccountPublic(ctx, targetAccount)
|
apiAccount, err = p.converter.AccountToAPIAccountPublic(ctx, targetAccount)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %w", err))
|
err := gtserror.Newf("error converting account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiAccount, nil
|
return apiAccount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWeb returns the web model of a local account by username.
|
||||||
|
func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.Account, gtserror.WithCode) {
|
||||||
|
targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.New("account not found")
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
err := gtserror.Newf("db error getting account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error converting account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return webAccount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomCSSForUsername returns custom css for the given local username.
|
||||||
|
func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) {
|
||||||
|
customCSS, err := p.state.DB.GetAccountCustomCSSByUsername(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return "", gtserror.NewErrorNotFound(gtserror.New("account not found"))
|
||||||
|
}
|
||||||
|
return "", gtserror.NewErrorInternalError(gtserror.Newf("db error: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return customCSS, nil
|
||||||
|
}
|
||||||
|
|
|
@ -204,11 +204,16 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form.AvatarDescription != nil {
|
||||||
|
desc := text.SanitizeToPlaintext(*form.AvatarDescription)
|
||||||
|
form.AvatarDescription = util.Ptr(desc)
|
||||||
|
}
|
||||||
|
|
||||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||||
avatarInfo, errWithCode := p.UpdateAvatar(ctx,
|
avatarInfo, errWithCode := p.UpdateAvatar(ctx,
|
||||||
account,
|
account,
|
||||||
form.Avatar,
|
form.Avatar,
|
||||||
nil,
|
form.AvatarDescription,
|
||||||
)
|
)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
|
@ -216,13 +221,29 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
account.AvatarMediaAttachmentID = avatarInfo.ID
|
account.AvatarMediaAttachmentID = avatarInfo.ID
|
||||||
account.AvatarMediaAttachment = avatarInfo
|
account.AvatarMediaAttachment = avatarInfo
|
||||||
log.Tracef(ctx, "new avatar info for account %s is %+v", account.ID, avatarInfo)
|
log.Tracef(ctx, "new avatar info for account %s is %+v", account.ID, avatarInfo)
|
||||||
|
} else if form.AvatarDescription != nil && account.AvatarMediaAttachment != nil {
|
||||||
|
// Update just existing description if possible.
|
||||||
|
account.AvatarMediaAttachment.Description = *form.AvatarDescription
|
||||||
|
if err := p.state.DB.UpdateAttachment(
|
||||||
|
ctx,
|
||||||
|
account.AvatarMediaAttachment,
|
||||||
|
"description",
|
||||||
|
); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating account avatar description: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.HeaderDescription != nil {
|
||||||
|
desc := text.SanitizeToPlaintext(*form.HeaderDescription)
|
||||||
|
form.HeaderDescription = util.Ptr(desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Header != nil && form.Header.Size != 0 {
|
if form.Header != nil && form.Header.Size != 0 {
|
||||||
headerInfo, errWithCode := p.UpdateHeader(ctx,
|
headerInfo, errWithCode := p.UpdateHeader(ctx,
|
||||||
account,
|
account,
|
||||||
form.Header,
|
form.Header,
|
||||||
nil,
|
form.HeaderDescription,
|
||||||
)
|
)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
|
@ -230,6 +251,17 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
account.HeaderMediaAttachmentID = headerInfo.ID
|
account.HeaderMediaAttachmentID = headerInfo.ID
|
||||||
account.HeaderMediaAttachment = headerInfo
|
account.HeaderMediaAttachment = headerInfo
|
||||||
log.Tracef(ctx, "new header info for account %s is %+v", account.ID, headerInfo)
|
log.Tracef(ctx, "new header info for account %s is %+v", account.ID, headerInfo)
|
||||||
|
} else if form.HeaderDescription != nil && account.HeaderMediaAttachment != nil {
|
||||||
|
// Update just existing description if possible.
|
||||||
|
account.HeaderMediaAttachment.Description = *form.HeaderDescription
|
||||||
|
if err := p.state.DB.UpdateAttachment(
|
||||||
|
ctx,
|
||||||
|
account.HeaderMediaAttachment,
|
||||||
|
"description",
|
||||||
|
); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating account avatar description: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Locked != nil {
|
if form.Locked != nil {
|
||||||
|
|
|
@ -162,6 +162,38 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountToWebAccount converts a gts model account into an
|
||||||
|
// api representation suitable for serving into a web template.
|
||||||
|
//
|
||||||
|
// Should only be used when preparing to template an account,
|
||||||
|
// callers looking to serialize an account into a model for
|
||||||
|
// serving over the client API should always use one of the
|
||||||
|
// AccountToAPIAccount functions instead.
|
||||||
|
func (c *Converter) AccountToWebAccount(
|
||||||
|
ctx context.Context,
|
||||||
|
a *gtsmodel.Account,
|
||||||
|
) (*apimodel.Account, error) {
|
||||||
|
webAccount, err := c.AccountToAPIAccountPublic(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set additional avatar information for
|
||||||
|
// serving the avatar in a nice photobox.
|
||||||
|
if a.AvatarMediaAttachment != nil {
|
||||||
|
avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, a.AvatarMediaAttachment)
|
||||||
|
if err != nil {
|
||||||
|
// This is just extra data so just
|
||||||
|
// log but don't return any error.
|
||||||
|
log.Errorf(ctx, "error converting account avatar attachment: %v", err)
|
||||||
|
} else {
|
||||||
|
webAccount.AvatarAttachment = &avatarAttachment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return webAccount, nil
|
||||||
|
}
|
||||||
|
|
||||||
// accountToAPIAccountPublic provides all the logic for AccountToAPIAccount, MINUS fetching moved account, to prevent possible recursion.
|
// accountToAPIAccountPublic provides all the logic for AccountToAPIAccount, MINUS fetching moved account, to prevent possible recursion.
|
||||||
func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
|
|
||||||
|
@ -210,18 +242,22 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
var (
|
var (
|
||||||
aviURL string
|
aviURL string
|
||||||
aviURLStatic string
|
aviURLStatic string
|
||||||
|
aviDesc string
|
||||||
headerURL string
|
headerURL string
|
||||||
headerURLStatic string
|
headerURLStatic string
|
||||||
|
headerDesc string
|
||||||
)
|
)
|
||||||
|
|
||||||
if a.AvatarMediaAttachment != nil {
|
if a.AvatarMediaAttachment != nil {
|
||||||
aviURL = a.AvatarMediaAttachment.URL
|
aviURL = a.AvatarMediaAttachment.URL
|
||||||
aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL
|
aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL
|
||||||
|
aviDesc = a.AvatarMediaAttachment.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.HeaderMediaAttachment != nil {
|
if a.HeaderMediaAttachment != nil {
|
||||||
headerURL = a.HeaderMediaAttachment.URL
|
headerURL = a.HeaderMediaAttachment.URL
|
||||||
headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
|
headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
|
||||||
|
headerDesc = a.HeaderMediaAttachment.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert account gts model fields to front api model fields
|
// convert account gts model fields to front api model fields
|
||||||
|
@ -294,32 +330,34 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
// can be populated directly below.
|
// can be populated directly below.
|
||||||
|
|
||||||
accountFrontend := &apimodel.Account{
|
accountFrontend := &apimodel.Account{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Username: a.Username,
|
Username: a.Username,
|
||||||
Acct: acct,
|
Acct: acct,
|
||||||
DisplayName: a.DisplayName,
|
DisplayName: a.DisplayName,
|
||||||
Locked: locked,
|
Locked: locked,
|
||||||
Discoverable: discoverable,
|
Discoverable: discoverable,
|
||||||
Bot: bot,
|
Bot: bot,
|
||||||
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
CreatedAt: util.FormatISO8601(a.CreatedAt),
|
||||||
Note: a.Note,
|
Note: a.Note,
|
||||||
URL: a.URL,
|
URL: a.URL,
|
||||||
Avatar: aviURL,
|
Avatar: aviURL,
|
||||||
AvatarStatic: aviURLStatic,
|
AvatarStatic: aviURLStatic,
|
||||||
Header: headerURL,
|
AvatarDescription: aviDesc,
|
||||||
HeaderStatic: headerURLStatic,
|
Header: headerURL,
|
||||||
FollowersCount: followersCount,
|
HeaderStatic: headerURLStatic,
|
||||||
FollowingCount: followingCount,
|
HeaderDescription: headerDesc,
|
||||||
StatusesCount: statusesCount,
|
FollowersCount: followersCount,
|
||||||
LastStatusAt: lastStatusAt,
|
FollowingCount: followingCount,
|
||||||
Emojis: apiEmojis,
|
StatusesCount: statusesCount,
|
||||||
Fields: fields,
|
LastStatusAt: lastStatusAt,
|
||||||
Suspended: !a.SuspendedAt.IsZero(),
|
Emojis: apiEmojis,
|
||||||
Theme: theme,
|
Fields: fields,
|
||||||
CustomCSS: customCSS,
|
Suspended: !a.SuspendedAt.IsZero(),
|
||||||
EnableRSS: enableRSS,
|
Theme: theme,
|
||||||
HideCollections: hideCollections,
|
CustomCSS: customCSS,
|
||||||
Role: role,
|
EnableRSS: enableRSS,
|
||||||
|
HideCollections: hideCollections,
|
||||||
|
Role: role,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bodge default avatar + header in,
|
// Bodge default avatar + header in,
|
||||||
|
|
|
@ -57,8 +57,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -108,8 +110,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -199,8 +203,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -247,8 +253,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
@ -291,8 +299,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
||||||
"url": "http://localhost:8080/@the_mighty_zork",
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_description": "a green goblin looking nasty",
|
||||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
"followers_count": 2,
|
"followers_count": 2,
|
||||||
"following_count": 2,
|
"following_count": 2,
|
||||||
"statuses_count": 7,
|
"statuses_count": 7,
|
||||||
|
|
|
@ -28,7 +28,6 @@ import (
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) profileGETHandler(c *gin.Context) {
|
func (m *Module) profileGETHandler(c *gin.Context) {
|
||||||
|
@ -79,16 +78,8 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
// text/html has been requested. Proceed with getting the web view of the account.
|
// text/html has been requested. Proceed with getting the web view of the account.
|
||||||
|
|
||||||
// Don't require auth for web endpoints, but do take it if it was provided.
|
|
||||||
// authed.Account might end up nil here, but that's fine in case of public pages.
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
|
||||||
if err != nil {
|
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the target account so we can do some checks on it.
|
// Fetch the target account so we can do some checks on it.
|
||||||
targetAccount, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, targetUsername)
|
targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||||
return
|
return
|
||||||
|
|
|
@ -29,7 +29,6 @@ import (
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) threadGETHandler(c *gin.Context) {
|
func (m *Module) threadGETHandler(c *gin.Context) {
|
||||||
|
@ -88,16 +87,8 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
// text/html has been requested. Proceed with getting the web view of the status.
|
// text/html has been requested. Proceed with getting the web view of the status.
|
||||||
|
|
||||||
// Don't require auth for web endpoints, but do take it if it was provided.
|
|
||||||
// authed.Account might end up nil here, but that's fine in case of public pages.
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
|
||||||
if err != nil {
|
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the target account so we can do some checks on it.
|
// Fetch the target account so we can do some checks on it.
|
||||||
targetAccount, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, targetUsername)
|
targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||||
return
|
return
|
||||||
|
|
|
@ -969,7 +969,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
Description: "A very old-school screenshot of the original team fortress mod for quake ",
|
Description: "A very old-school screenshot of the original team fortress mod for quake",
|
||||||
ScheduledStatusID: "",
|
ScheduledStatusID: "",
|
||||||
Blurhash: "L26j{^WCs+R-N}jsxWj@4;WWxDoK",
|
Blurhash: "L26j{^WCs+R-N}jsxWj@4;WWxDoK",
|
||||||
Processing: 2,
|
Processing: 2,
|
||||||
|
|
|
@ -82,18 +82,37 @@
|
||||||
margin-top: calc(-1 * $overlap);
|
margin-top: calc(-1 * $overlap);
|
||||||
gap: 0 1rem;
|
gap: 0 1rem;
|
||||||
|
|
||||||
.avatar {
|
.avatar-image-wrapper {
|
||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
height: $avatar-size;
|
|
||||||
width: $avatar-size;
|
|
||||||
border: 0.2rem solid $avatar-border;
|
border: 0.2rem solid $avatar-border;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
overflow: hidden; /* prevents image extending beyond rounded borders */
|
|
||||||
|
/*
|
||||||
|
Wrapper always same
|
||||||
|
size + proportions no
|
||||||
|
matter image inside.
|
||||||
|
*/
|
||||||
|
height: $avatar-size;
|
||||||
|
width: $avatar-size;
|
||||||
|
|
||||||
img {
|
.avatar {
|
||||||
|
/*
|
||||||
|
Fit 100% of the wrapper.
|
||||||
|
*/
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Normalize non-square images.
|
||||||
|
*/
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent image extending
|
||||||
|
beyond rounded borders.
|
||||||
|
*/
|
||||||
|
border-radius: $br-inner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,11 @@ export default function FakeProfile({ avatar, header, display_name, username, ro
|
||||||
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="basic-info" aria-hidden="true">
|
<div className="basic-info" aria-hidden="true">
|
||||||
<a className="avatar" href={avatar}>
|
<div className="avatar-image-wrapper">
|
||||||
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
<a href={avatar}>
|
||||||
</a>
|
<img className="avatar" src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<dl className="namerole">
|
<dl className="namerole">
|
||||||
<dt className="sr-only">Display name</dt>
|
<dt className="sr-only">Display name</dt>
|
||||||
<dd className="displayname text-cutoff">{display_name.trim().length > 0 ? display_name : username}</dd>
|
<dd className="displayname text-cutoff">{display_name.trim().length > 0 ? display_name : username}</dd>
|
||||||
|
|
|
@ -400,12 +400,13 @@ section.with-sidebar > form {
|
||||||
width: 24rem;
|
width: 24rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.file-input-with-image-description {
|
|
||||||
display: flex;
|
.file-input-with-image-description {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
justify-content: space-around;
|
flex-direction: column;
|
||||||
}
|
justify-content: space-around;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -422,11 +423,13 @@ section.with-sidebar > form {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile {
|
.user-profile {
|
||||||
|
.profile {
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
display: grid;
|
display: flex;
|
||||||
max-width: 60rem;
|
flex-direction: column;
|
||||||
grid-template-columns: 70% 30%;
|
|
||||||
grid-template-rows: auto;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
.files {
|
.files {
|
||||||
|
|
|
@ -93,7 +93,9 @@ function UserProfileForm({ data: profile }) {
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
avatar: useFileInput("avatar", { withPreview: true }),
|
avatar: useFileInput("avatar", { withPreview: true }),
|
||||||
|
avatarDescription: useTextInput("avatar_description", { source: profile }),
|
||||||
header: useFileInput("header", { withPreview: true }),
|
header: useFileInput("header", { withPreview: true }),
|
||||||
|
headerDescription: useTextInput("header_description", { source: profile }),
|
||||||
displayName: useTextInput("display_name", { source: profile }),
|
displayName: useTextInput("display_name", { source: profile }),
|
||||||
note: useTextInput("note", { source: profile, valueSelector: (p) => p.source?.note }),
|
note: useTextInput("note", { source: profile, valueSelector: (p) => p.source?.note }),
|
||||||
bot: useBoolInput("bot", { source: profile }),
|
bot: useBoolInput("bot", { source: profile }),
|
||||||
|
@ -131,21 +133,33 @@ function UserProfileForm({ data: profile }) {
|
||||||
username={profile.username}
|
username={profile.username}
|
||||||
role={profile.role}
|
role={profile.role}
|
||||||
/>
|
/>
|
||||||
<div className="files">
|
|
||||||
<div>
|
<div className="file-input-with-image-description">
|
||||||
<FileInput
|
<FileInput
|
||||||
label="Header"
|
label="Header"
|
||||||
field={form.header}
|
field={form.header}
|
||||||
accept="image/*"
|
accept="image/png, image/jpeg, image/webp, image/gif"
|
||||||
/>
|
/>
|
||||||
</div>
|
<TextInput
|
||||||
<div>
|
field={form.headerDescription}
|
||||||
<FileInput
|
label="Header image description"
|
||||||
label="Avatar"
|
placeholder="A green field with pink flowers."
|
||||||
field={form.avatar}
|
autoCapitalize="sentences"
|
||||||
accept="image/*"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="file-input-with-image-description">
|
||||||
|
<FileInput
|
||||||
|
label="Avatar (1:1 images look best)"
|
||||||
|
field={form.avatar}
|
||||||
|
accept="image/png, image/jpeg, image/webp, image/gif"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
field={form.avatarDescription}
|
||||||
|
label="Avatar image description"
|
||||||
|
placeholder="A cute drawing of a smiling sloth."
|
||||||
|
autoCapitalize="sentences"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="theme">
|
<div className="theme">
|
||||||
|
|
|
@ -35,6 +35,78 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "defaultAvatarDimension" -}}
|
||||||
|
{{- /* 136 is the default width/height for 8.5rem avatars, double it to get a good look when expanded. */ -}}
|
||||||
|
272
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "avatarWidth" -}}
|
||||||
|
{{- with .account }}
|
||||||
|
{{- if isNil .AvatarAttachment -}}
|
||||||
|
{{- template "defaultAvatarDimension" . -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- /* Use the avatar's proper dimensions. */ -}}
|
||||||
|
{{- .AvatarAttachment.Meta.Original.Width -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "avatarHeight" -}}
|
||||||
|
{{- with .account }}
|
||||||
|
{{- if isNil .AvatarAttachment -}}
|
||||||
|
{{- template "defaultAvatarDimension" . -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- /* Use the avatar's proper dimensions. */ -}}
|
||||||
|
{{- .AvatarAttachment.Meta.Original.Height -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "avatarAlt" -}}
|
||||||
|
Avatar for {{ .account.Username -}}
|
||||||
|
{{- if .account.AvatarDescription }}
|
||||||
|
{{- /* Add the avatar's image description. */ -}}
|
||||||
|
: {{ .account.AvatarDescription -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "headerAlt" -}}
|
||||||
|
Header for {{ .account.Username -}}
|
||||||
|
{{- if .account.HeaderDescription }}
|
||||||
|
{{- /* Add the header's image description. */ -}}
|
||||||
|
: {{ .account.HeaderDescription -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "avatar" -}}
|
||||||
|
{{- with . }}
|
||||||
|
<div
|
||||||
|
class="media photoswipe-gallery odd single avatar-image-wrapper"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="photoswipe-slide"
|
||||||
|
href="{{- .account.Avatar -}}"
|
||||||
|
target="_blank"
|
||||||
|
data-pswp-width="{{- template "avatarWidth" . -}}px"
|
||||||
|
data-pswp-height="{{- template "avatarHeight" . -}}px"
|
||||||
|
data-cropped="true"
|
||||||
|
alt="{{- template "avatarAlt" . -}}"
|
||||||
|
title="{{- template "avatarAlt" . -}}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
src="{{- .account.Avatar -}}"
|
||||||
|
alt="{{- template "avatarAlt" . -}}"
|
||||||
|
title="{{- template "avatarAlt" . -}}"
|
||||||
|
width="{{- template "avatarWidth" . -}}"
|
||||||
|
height="{{- template "avatarHeight" . -}}"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- with . }}
|
{{- with . }}
|
||||||
<main class="profile">
|
<main class="profile">
|
||||||
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
|
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
|
||||||
|
@ -45,18 +117,14 @@
|
||||||
<div class="header-image-wrapper">
|
<div class="header-image-wrapper">
|
||||||
<img
|
<img
|
||||||
src="{{- .account.Header -}}"
|
src="{{- .account.Header -}}"
|
||||||
alt="Header for {{ .account.Username -}}"
|
alt="{{- template "headerAlt" . -}}"
|
||||||
title="Header for {{ .account.Username -}}"
|
title="{{- template "headerAlt" . -}}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="basic-info">
|
<div class="basic-info">
|
||||||
<a class="avatar" href="{{- .account.Avatar -}}">
|
{{- with . }}
|
||||||
<img
|
{{- include "avatar" . | indent 3 }}
|
||||||
src="{{- .account.Avatar -}}"
|
{{- end }}
|
||||||
alt="Avatar for {{ .account.Username -}}"
|
|
||||||
title="Avatar for {{ .account.Username -}}"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<dl class="namerole">
|
<dl class="namerole">
|
||||||
<dt class="sr-only">Display name</dt>
|
<dt class="sr-only">Display name</dt>
|
||||||
<dd class="displayname text-cutoff">
|
<dd class="displayname text-cutoff">
|
||||||
|
|
Loading…
Reference in a new issue