mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 02:32:25 -05:00
[feature] Allow user to choose "gallery" style layout for web view of profile (#3917)
* [feature] Allow user to choose "gallery" style web layout * find a bug and squish it up and all day long you'll have good luck * just a sec * [performance] reindex public timeline + tinker with query a bit * fiddling * should be good now * last bit of finagling, i'm done now i prommy * panic normally
This commit is contained in:
parent
f46e490c30
commit
b6e481d63e
82 changed files with 2921 additions and 1171 deletions
|
|
@ -153,6 +153,14 @@ import (
|
|||
// "none": show no posts on the web, not even Public ones.
|
||||
// type: string
|
||||
// -
|
||||
// name: web_layout
|
||||
// in: formData
|
||||
// description: |-
|
||||
// Layout to use for the web view of the account.
|
||||
// "microblog": default, classic microblog layout.
|
||||
// "gallery": gallery layout with media only.
|
||||
// type: string
|
||||
// -
|
||||
// name: fields_attributes[0][name]
|
||||
// in: formData
|
||||
// description: Name of 1st profile field to be added to this account's profile.
|
||||
|
|
@ -351,7 +359,8 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
|
|||
form.CustomCSS == nil &&
|
||||
form.EnableRSS == nil &&
|
||||
form.HideCollections == nil &&
|
||||
form.WebVisibility == nil) {
|
||||
form.WebVisibility == nil &&
|
||||
form.WebLayout == nil) {
|
||||
return nil, errors.New("empty form submitted")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -369,16 +369,16 @@ func (suite *AccountSearchTestSuite) TestSearchAFollowing() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
if l := len(accounts); l != 5 {
|
||||
suite.FailNow("", "expected length %d got %d", 5, l)
|
||||
if l := len(accounts); l != 6 {
|
||||
suite.FailNow("", "expected length %d got %d", 6, l)
|
||||
}
|
||||
|
||||
usernames := make([]string, 0, 5)
|
||||
usernames := make([]string, 0, 6)
|
||||
for _, account := range accounts {
|
||||
usernames = append(usernames, account.Username)
|
||||
}
|
||||
|
||||
suite.EqualValues([]string{"her_fuckin_maj", "foss_satan", "1happyturtle", "the_mighty_zork", "admin"}, usernames)
|
||||
suite.EqualValues([]string{"her_fuckin_maj", "media_mogul", "foss_satan", "1happyturtle", "the_mighty_zork", "admin"}, usernames)
|
||||
}
|
||||
|
||||
func (suite *AccountSearchTestSuite) TestSearchANotFollowing() {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,69 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"group": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "01JPCMD83Y4WR901094YES3QC5",
|
||||
"username": "media_mogul",
|
||||
"domain": null,
|
||||
"created_at": "2025-03-15T11:08:00.000Z",
|
||||
"email": "media.mogul@example.org",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"id": "user",
|
||||
"name": "user",
|
||||
"color": "",
|
||||
"permissions": "0",
|
||||
"highlighted": false
|
||||
},
|
||||
"confirmed": true,
|
||||
"approved": true,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01JPCMD83Y4WR901094YES3QC5",
|
||||
"username": "media_mogul",
|
||||
"acct": "media_mogul",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"created_at": "2025-03-15T11:08:00.000Z",
|
||||
"note": "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
|
||||
"url": "http://localhost:8080/@media_mogul",
|
||||
"avatar": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
|
||||
"avatar_description": "DESCRIPTION_GOES_HERE",
|
||||
"avatar_media_id": "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
|
||||
"header": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
|
||||
"header_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
|
||||
"header_description": "DESCRIPTION_GOES_HERE",
|
||||
"header_media_id": "01JPHRB7F2RXPTEQFRYC85EPD9",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2025-03-15",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
"name": "I'm going to post a lot of",
|
||||
"value": "media!",
|
||||
"verified_at": null
|
||||
},
|
||||
{
|
||||
"name": "and there's nothing",
|
||||
"value": "you can do about it",
|
||||
"verified_at": null
|
||||
}
|
||||
],
|
||||
"enable_rss": true,
|
||||
"group": false
|
||||
},
|
||||
"created_by_application_id": "01HT5P2YHDMPAAD500NDAY8JW1"
|
||||
},
|
||||
{
|
||||
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
"username": "the_mighty_zork",
|
||||
|
|
@ -547,18 +610,18 @@ func (suite *AccountsGetTestSuite) TestAccountsMinID() {
|
|||
}
|
||||
|
||||
link := recorder.Header().Get("Link")
|
||||
suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=1&max_id=%2F%40localhost%3A8080>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=1&min_id=%2F%40localhost%3A8080>; rel="prev"`, link)
|
||||
suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=1&max_id=%2F%40media_mogul>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=1&min_id=%2F%40media_mogul>; rel="prev"`, link)
|
||||
|
||||
suite.Equal(`[
|
||||
{
|
||||
"id": "01AY6P665V14JJR0AFVRT7311Y",
|
||||
"username": "localhost:8080",
|
||||
"id": "01JPCMD83Y4WR901094YES3QC5",
|
||||
"username": "media_mogul",
|
||||
"domain": null,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"email": "",
|
||||
"created_at": "2025-03-15T11:08:00.000Z",
|
||||
"email": "media.mogul@example.org",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"id": "user",
|
||||
|
|
@ -567,35 +630,51 @@ func (suite *AccountsGetTestSuite) TestAccountsMinID() {
|
|||
"permissions": "0",
|
||||
"highlighted": false
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"confirmed": true,
|
||||
"approved": true,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01AY6P665V14JJR0AFVRT7311Y",
|
||||
"username": "localhost:8080",
|
||||
"acct": "localhost:8080",
|
||||
"id": "01JPCMD83Y4WR901094YES3QC5",
|
||||
"username": "media_mogul",
|
||||
"acct": "media_mogul",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@localhost:8080",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_description": "Flat gray background (default header).",
|
||||
"created_at": "2025-03-15T11:08:00.000Z",
|
||||
"note": "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
|
||||
"url": "http://localhost:8080/@media_mogul",
|
||||
"avatar": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
|
||||
"avatar_description": "DESCRIPTION_GOES_HERE",
|
||||
"avatar_media_id": "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
|
||||
"header": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
|
||||
"header_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
|
||||
"header_description": "DESCRIPTION_GOES_HERE",
|
||||
"header_media_id": "01JPHRB7F2RXPTEQFRYC85EPD9",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": null,
|
||||
"statuses_count": 2,
|
||||
"last_status_at": "2025-03-15",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"fields": [
|
||||
{
|
||||
"name": "I'm going to post a lot of",
|
||||
"value": "media!",
|
||||
"verified_at": null
|
||||
},
|
||||
{
|
||||
"name": "and there's nothing",
|
||||
"value": "you can do about it",
|
||||
"verified_at": null
|
||||
}
|
||||
],
|
||||
"enable_rss": true,
|
||||
"group": false
|
||||
}
|
||||
},
|
||||
"created_by_application_id": "01HT5P2YHDMPAAD500NDAY8JW1"
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
@ -301,8 +301,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
@ -444,8 +444,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
@ -638,8 +638,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
@ -803,8 +803,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
|
||||
"thumbnail_type": "image/gif",
|
||||
|
|
@ -987,8 +987,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
|
|||
|
|
@ -915,7 +915,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 5)
|
||||
suite.Len(searchResult.Accounts, 6)
|
||||
suite.Len(searchResult.Statuses, 9)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
|
@ -1130,7 +1130,7 @@ func (suite *SearchGetTestSuite) TestSearchAAccounts() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 5)
|
||||
suite.Len(searchResult.Accounts, 6)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ type WebAccount struct {
|
|||
// Only set if this account had a header set
|
||||
// (and not just the default "blank" image.)
|
||||
HeaderAttachment *WebAttachment `json:"-"`
|
||||
|
||||
// Layout for this account (microblog, gallery).
|
||||
WebLayout string `json:"-"`
|
||||
}
|
||||
|
||||
// MutedAccount extends Account with a field used only by the muted user list.
|
||||
|
|
@ -240,6 +243,10 @@ type UpdateCredentialsRequest struct {
|
|||
// Visibility of statuses to show via the web view.
|
||||
// "none", "public" (default), or "unlisted" (which includes public as well).
|
||||
WebVisibility *string `form:"web_visibility" json:"web_visibility"`
|
||||
// Layout to use for the web view of the account.
|
||||
// "microblog": default, classic microblog layout.
|
||||
// "gallery": gallery layout with media only.
|
||||
WebLayout *string `form:"web_layout" json:"web_layout"`
|
||||
}
|
||||
|
||||
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ type WebAttachment struct {
|
|||
// MIME type of
|
||||
// the thumbnail.
|
||||
PreviewMIMEType string
|
||||
|
||||
// Link to the URL of the parent
|
||||
// status of this attachment.
|
||||
ParentStatusLink string
|
||||
}
|
||||
|
||||
// MediaMeta models media metadata.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ type Source struct {
|
|||
// "unlisted" = show Public *and* Unlisted visibility posts on the web.
|
||||
// "none" = show no posts on the web, not even Public ones.
|
||||
WebVisibility Visibility `json:"web_visibility"`
|
||||
// Layout to use for the web view of the account.
|
||||
// "microblog": default, classic microblog layout.
|
||||
// "gallery": gallery layout with media only.
|
||||
WebLayout string `json:"web_layout"`
|
||||
// Whether new statuses should be marked sensitive by default.
|
||||
Sensitive bool `json:"sensitive"`
|
||||
// The default posting language for new statuses.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ type Account interface {
|
|||
// returning statuses that should be visible via the web view of a *LOCAL* account.
|
||||
//
|
||||
// In the case of no statuses, this function will return db.ErrNoEntries.
|
||||
GetAccountWebStatuses(ctx context.Context, account *gtsmodel.Account, limit int, maxID string) ([]*gtsmodel.Status, error)
|
||||
GetAccountWebStatuses(ctx context.Context, account *gtsmodel.Account, mediaOnly bool, limit int, maxID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetInstanceAccount returns the instance account for the given domain.
|
||||
// If domain is empty, this instance account will be returned.
|
||||
|
|
|
|||
|
|
@ -878,6 +878,29 @@ func (a *accountDB) GetAccountFaves(ctx context.Context, accountID string) ([]*g
|
|||
return *faves, nil
|
||||
}
|
||||
|
||||
func qMediaOnly(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
// Attachments are stored as a json object; this
|
||||
// implementation differs between SQLite and Postgres,
|
||||
// so we have to be thorough to cover all eventualities
|
||||
return q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
switch d := q.Dialect().Name(); d {
|
||||
case dialect.PG:
|
||||
return q.
|
||||
Where("? IS NOT NULL", bun.Ident("status.attachments")).
|
||||
Where("? != '{}'", bun.Ident("status.attachments"))
|
||||
|
||||
case dialect.SQLite:
|
||||
return q.
|
||||
Where("? IS NOT NULL", bun.Ident("status.attachments")).
|
||||
Where("? != 'null'", bun.Ident("status.attachments")).
|
||||
Where("? != '[]'", bun.Ident("status.attachments"))
|
||||
|
||||
default:
|
||||
panic("dialect " + d.String() + " was neither pg nor sqlite")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, error) {
|
||||
// Ensure reasonable
|
||||
if limit < 0 {
|
||||
|
|
@ -918,28 +941,9 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
|
|||
q = q.Where("? IS NULL", bun.Ident("status.boost_of_id"))
|
||||
}
|
||||
|
||||
// Respect media-only preference.
|
||||
if mediaOnly {
|
||||
// Attachments are stored as a json object; this
|
||||
// implementation differs between SQLite and Postgres,
|
||||
// so we have to be thorough to cover all eventualities
|
||||
q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
switch a.db.Dialect().Name() {
|
||||
case dialect.PG:
|
||||
return q.
|
||||
Where("? IS NOT NULL", bun.Ident("status.attachments")).
|
||||
Where("? != '{}'", bun.Ident("status.attachments"))
|
||||
case dialect.SQLite:
|
||||
return q.
|
||||
Where("? IS NOT NULL", bun.Ident("status.attachments")).
|
||||
Where("? != ''", bun.Ident("status.attachments")).
|
||||
Where("? != 'null'", bun.Ident("status.attachments")).
|
||||
Where("? != '{}'", bun.Ident("status.attachments")).
|
||||
Where("? != '[]'", bun.Ident("status.attachments"))
|
||||
default:
|
||||
log.Panic(ctx, "db dialect was neither pg nor sqlite")
|
||||
return q
|
||||
}
|
||||
})
|
||||
q = qMediaOnly(q)
|
||||
}
|
||||
|
||||
if publicOnly {
|
||||
|
|
@ -1018,6 +1022,7 @@ func (a *accountDB) GetAccountPinnedStatuses(ctx context.Context, accountID stri
|
|||
func (a *accountDB) GetAccountWebStatuses(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
mediaOnly bool,
|
||||
limit int,
|
||||
maxID string,
|
||||
) ([]*gtsmodel.Status, error) {
|
||||
|
|
@ -1046,10 +1051,7 @@ func (a *accountDB) GetAccountWebStatuses(
|
|||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||
// Select only IDs from table
|
||||
Column("status.id").
|
||||
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||
// Don't show replies or boosts.
|
||||
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
|
||||
Where("? IS NULL", bun.Ident("status.boost_of_id"))
|
||||
Where("? = ?", bun.Ident("status.account_id"), account.ID)
|
||||
|
||||
// Select statuses for this account according
|
||||
// to their web visibility preference.
|
||||
|
|
@ -1074,10 +1076,19 @@ func (a *accountDB) GetAccountWebStatuses(
|
|||
)
|
||||
}
|
||||
|
||||
// Don't show local-only statuses on the web view.
|
||||
q = q.Where("? = ?", bun.Ident("status.federated"), true)
|
||||
// Don't show replies, boosts, or
|
||||
// local-only statuses on the web view.
|
||||
q = q.
|
||||
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
|
||||
Where("? IS NULL", bun.Ident("status.boost_of_id")).
|
||||
Where("? = ?", bun.Ident("status.federated"), true)
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
// Respect media-only preference.
|
||||
if mediaOnly {
|
||||
q = qMediaOnly(q)
|
||||
}
|
||||
|
||||
// Return only statuses LOWER (ie., older) than maxID
|
||||
if maxID == "" {
|
||||
maxID = id.Highest
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ func (suite *AccountTestSuite) TestGetAccountStatuses() {
|
|||
suite.Len(statuses, 9)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountWebStatusesMediaOnly() {
|
||||
statuses, err := suite.db.GetAccountWebStatuses(context.Background(), suite.testAccounts["local_account_3"], true, 20, "")
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 2)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
||||
// get the first page
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, "", "", false, false)
|
||||
|
|
@ -490,7 +496,7 @@ func (suite *AccountTestSuite) TestGetAccountsAll() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(accounts, 9)
|
||||
suite.Len(accounts, 10)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountsMaxID() {
|
||||
|
|
@ -564,7 +570,7 @@ func (suite *AccountTestSuite) TestGetAccountsMinID() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(accounts, 3)
|
||||
suite.Len(accounts, 4)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountsModsOnly() {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
|
|||
s := []*gtsmodel.Status{}
|
||||
err := suite.db.GetAll(context.Background(), &s)
|
||||
suite.NoError(err)
|
||||
suite.Len(s, 28)
|
||||
suite.Len(s, 30)
|
||||
}
|
||||
|
||||
func (suite *BasicTestSuite) TestGetAllNotNull() {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type InstanceTestSuite struct {
|
|||
func (suite *InstanceTestSuite) TestCountInstanceUsers() {
|
||||
count, err := suite.db.CountInstanceUsers(context.Background(), config.GetHost())
|
||||
suite.NoError(err)
|
||||
suite.Equal(4, count)
|
||||
suite.Equal(5, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
|
||||
|
|
@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
|
|||
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
|
||||
suite.NoError(err)
|
||||
suite.Equal(21, count)
|
||||
suite.Equal(23, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
// 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 migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Add new column to settings.
|
||||
if _, err := tx.
|
||||
NewAddColumn().
|
||||
Table("account_settings").
|
||||
ColumnExpr(
|
||||
"? SMALLINT NOT NULL DEFAULT ?",
|
||||
bun.Ident("web_layout"), 1,
|
||||
).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop existing statuses web index as it's out of date.
|
||||
log.Info(ctx, "updating statuses_profile_web_view_idx, this may take a while, please wait!")
|
||||
if _, err := tx.
|
||||
NewDropIndex().
|
||||
Index("statuses_profile_web_view_idx").
|
||||
IfExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note: "attachments" field is not included in
|
||||
// the index below as SQLite is fussy about using it,
|
||||
// and it prevents this index from being used
|
||||
// properly in non media-only queries.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("statuses").
|
||||
Index("statuses_profile_web_view_idx").
|
||||
Column(
|
||||
"account_id",
|
||||
"visibility",
|
||||
"in_reply_to_uri",
|
||||
"boost_of_id",
|
||||
"federated",
|
||||
).
|
||||
ColumnExpr("? DESC", bun.Ident("id")).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
package gtsmodel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -35,9 +36,51 @@ type AccountSettings struct {
|
|||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile.
|
||||
WebLayout WebLayout `bun:",nullzero,notnull,default:1"` // Layout to use when showing this profile via the web.
|
||||
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
||||
InteractionPolicyMutualsOnly *InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyUnlocked *InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyPublic *InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy.
|
||||
}
|
||||
|
||||
// WebLayout represents an account owner's
|
||||
// choice for how they want their profile to be
|
||||
// laid out via the web view, by default.
|
||||
type WebLayout enumType
|
||||
|
||||
const (
|
||||
WebLayoutUnknown WebLayout = 0
|
||||
|
||||
// "Classic" / default GtS microblog view.
|
||||
WebLayoutMicroblog WebLayout = 1
|
||||
|
||||
// 'gram-style gallery view with media only.
|
||||
WebLayoutGallery WebLayout = 2
|
||||
)
|
||||
|
||||
// String returns a stringified, frontend
|
||||
// API compatible form of WebLayout.
|
||||
func (wrm WebLayout) String() string {
|
||||
switch wrm {
|
||||
case WebLayoutMicroblog:
|
||||
return "microblog"
|
||||
case WebLayoutGallery:
|
||||
return "gallery"
|
||||
default:
|
||||
panic("invalid web layout")
|
||||
}
|
||||
}
|
||||
|
||||
// ParseWebLayout returns a web
|
||||
// layout from the given value.
|
||||
func ParseWebLayout(in string) WebLayout {
|
||||
switch strings.ToLower(in) {
|
||||
case "microblog":
|
||||
return WebLayoutMicroblog
|
||||
case "gallery":
|
||||
return WebLayoutGallery
|
||||
default:
|
||||
return WebLayoutUnknown
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,8 +115,20 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
|
|||
// Reuse the lastPostAt value for feed.Updated.
|
||||
feed.Updated = lastPostAt
|
||||
|
||||
// Retrieve latest statuses as they'd be shown on the web view of the account profile.
|
||||
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, rssFeedLength, "")
|
||||
// Retrieve latest statuses as they'd be shown
|
||||
// on the web view of the account profile.
|
||||
//
|
||||
// Take into account whether the user wants
|
||||
// their web view laid out in gallery mode.
|
||||
mediaOnly := account.Settings != nil &&
|
||||
account.Settings.WebLayout == gtsmodel.WebLayoutGallery
|
||||
statuses, err := p.state.DB.GetAccountWebStatuses(
|
||||
ctx,
|
||||
account,
|
||||
mediaOnly,
|
||||
rssFeedLength,
|
||||
"", // Latest posts from the top.
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err = fmt.Errorf("db error getting account web statuses: %w", err)
|
||||
return "", gtserror.NewErrorInternalError(err)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ func (p *Processor) StatusesGet(
|
|||
func (p *Processor) WebStatusesGet(
|
||||
ctx context.Context,
|
||||
targetAccountID string,
|
||||
mediaOnly bool,
|
||||
maxID string,
|
||||
) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||
|
|
@ -159,7 +160,13 @@ func (p *Processor) WebStatusesGet(
|
|||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, 10, maxID)
|
||||
statuses, err := p.state.DB.GetAccountWebStatuses(
|
||||
ctx,
|
||||
account,
|
||||
mediaOnly,
|
||||
20,
|
||||
maxID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -198,6 +205,7 @@ func (p *Processor) WebStatusesGet(
|
|||
func (p *Processor) WebStatusesGetPinned(
|
||||
ctx context.Context,
|
||||
targetAccountID string,
|
||||
mediaOnly bool,
|
||||
) ([]*apimodel.WebStatus, gtserror.WithCode) {
|
||||
statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, targetAccountID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
|
|
@ -206,6 +214,11 @@ func (p *Processor) WebStatusesGetPinned(
|
|||
|
||||
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
if mediaOnly && len(status.Attachments) == 0 {
|
||||
// No media, skip.
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure visible via the web.
|
||||
visible, err := p.visFilter.StatusVisible(ctx, nil, status)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -294,6 +294,18 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
|||
settingsColumns = append(settingsColumns, "web_visibility")
|
||||
}
|
||||
|
||||
if form.WebLayout != nil {
|
||||
webLayout := gtsmodel.ParseWebLayout(*form.WebLayout)
|
||||
if webLayout == gtsmodel.WebLayoutUnknown {
|
||||
const text = "web_layout must be one of microblog or gallery"
|
||||
err := errors.New(text)
|
||||
return nil, gtserror.NewErrorBadRequest(err, text)
|
||||
}
|
||||
|
||||
account.Settings.WebLayout = webLayout
|
||||
settingsColumns = append(settingsColumns, "web_layout")
|
||||
}
|
||||
|
||||
// We've parsed + set everything, do
|
||||
// necessary database updates now.
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ func LoadTemplates(engine *gin.Engine) error {
|
|||
|
||||
// Set additional "include" functions to render
|
||||
// provided template name using the base template.
|
||||
|
||||
// Include renders the given template with the given data.
|
||||
// Unlike `template`, `include` can be chained with `indent`
|
||||
// to produce nicely-indented HTML.
|
||||
funcMap["include"] = func(name string, data any) (template.HTML, error) {
|
||||
var buf strings.Builder
|
||||
err := tmpl.ExecuteTemplate(&buf, name, data)
|
||||
|
|
@ -85,6 +89,25 @@ func LoadTemplates(engine *gin.Engine) error {
|
|||
return noescape(buf.String()), err
|
||||
}
|
||||
|
||||
// includeIndex is like `include` but an index can be specified at
|
||||
// `.Index` and data will be nested at `.Item`. Useful when ranging.
|
||||
funcMap["includeIndex"] = func(name string, data any, index int) (template.HTML, error) {
|
||||
var buf strings.Builder
|
||||
withIndex := struct {
|
||||
Item any
|
||||
Index int
|
||||
}{
|
||||
Item: data,
|
||||
Index: index,
|
||||
}
|
||||
err := tmpl.ExecuteTemplate(&buf, name, withIndex)
|
||||
|
||||
// Template was already escaped by
|
||||
// ExecuteTemplate so we can trust it.
|
||||
return noescape(buf.String()), err
|
||||
}
|
||||
|
||||
// includeAttr is like `include` but for element attributes.
|
||||
funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) {
|
||||
var buf strings.Builder
|
||||
err := tmpl.ExecuteTemplate(&buf, name, data)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(23, pruned)
|
||||
suite.Equal(25, pruned)
|
||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(23, pruned)
|
||||
suite.Equal(25, pruned)
|
||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
|
||||
// Prune same again, nothing should be pruned this time.
|
||||
|
|
@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(28, pruned)
|
||||
suite.Equal(30, pruned)
|
||||
suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() {
|
|||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, pruned)
|
||||
suite.Equal(28, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
suite.Equal(30, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
func TestPruneTestSuite(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
@ -151,7 +151,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
@ -216,7 +216,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/1happyturtle#main-key",
|
||||
"owner": "http://localhost:8080/users/1happyturtle",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-06-04T13:12:00Z",
|
||||
"summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
|
||||
|
|
@ -298,7 +298,7 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
@ -360,7 +360,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/1happyturtle#main-key",
|
||||
"owner": "http://localhost:8080/users/1happyturtle",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-06-04T13:12:00Z",
|
||||
"summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
|
||||
|
|
@ -422,7 +422,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
@ -497,7 +497,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
|||
apiAccount.Source = &apimodel.Source{
|
||||
Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy),
|
||||
WebVisibility: c.VisToAPIVis(ctx, a.Settings.WebVisibility),
|
||||
WebLayout: a.Settings.WebLayout.String(),
|
||||
Sensitive: *a.Settings.Sensitive,
|
||||
Language: a.Settings.Language,
|
||||
StatusContentType: statusContentType,
|
||||
|
|
@ -222,6 +223,14 @@ func (c *Converter) AccountToWebAccount(
|
|||
}
|
||||
}
|
||||
|
||||
// Check for presence of settings before
|
||||
// populating settings-specific thingies,
|
||||
// as instance account doesn't store a
|
||||
// settings struct.
|
||||
if a.Settings != nil {
|
||||
webAccount.WebLayout = a.Settings.WebLayout.String()
|
||||
}
|
||||
|
||||
return webAccount, nil
|
||||
}
|
||||
|
||||
|
|
@ -1227,10 +1236,11 @@ func (c *Converter) StatusToWebStatus(
|
|||
for i, apiAttachment := range apiStatus.MediaAttachments {
|
||||
ogAttachment := ogAttachments[apiAttachment.ID]
|
||||
webStatus.MediaAttachments[i] = &apimodel.WebAttachment{
|
||||
Attachment: apiAttachment,
|
||||
Sensitive: apiStatus.Sensitive,
|
||||
MIMEType: ogAttachment.File.ContentType,
|
||||
PreviewMIMEType: ogAttachment.Thumbnail.ContentType,
|
||||
Attachment: apiAttachment,
|
||||
Sensitive: apiStatus.Sensitive,
|
||||
MIMEType: ogAttachment.File.ContentType,
|
||||
PreviewMIMEType: ogAttachment.Thumbnail.ContentType,
|
||||
ParentStatusLink: apiStatus.URL,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
|||
"source": {
|
||||
"privacy": "public",
|
||||
"web_visibility": "unlisted",
|
||||
"web_layout": "microblog",
|
||||
"sensitive": false,
|
||||
"language": "en",
|
||||
"status_content_type": "text/plain",
|
||||
|
|
@ -324,6 +325,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
|||
"source": {
|
||||
"privacy": "public",
|
||||
"web_visibility": "unlisted",
|
||||
"web_layout": "microblog",
|
||||
"sensitive": false,
|
||||
"language": "en",
|
||||
"status_content_type": "text/plain",
|
||||
|
|
@ -1815,7 +1817,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
"blurhash": "LKE3VIw}0KD%a2o{M|t7NFWps:t7",
|
||||
"Sensitive": true,
|
||||
"MIMEType": "image/jpg",
|
||||
"PreviewMIMEType": "image/webp"
|
||||
"PreviewMIMEType": "image/webp",
|
||||
"ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
|
||||
},
|
||||
{
|
||||
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
|
||||
|
|
@ -1830,7 +1833,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
|
||||
"Sensitive": true,
|
||||
"MIMEType": "",
|
||||
"PreviewMIMEType": ""
|
||||
"PreviewMIMEType": "",
|
||||
"ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
|
||||
},
|
||||
{
|
||||
"id": "01HE88YG74PVAB81PX2XA9F3FG",
|
||||
|
|
@ -1845,7 +1849,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
"blurhash": null,
|
||||
"Sensitive": true,
|
||||
"MIMEType": "",
|
||||
"PreviewMIMEType": ""
|
||||
"PreviewMIMEType": "",
|
||||
"ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
|
||||
}
|
||||
],
|
||||
"LanguageTag": "en",
|
||||
|
|
@ -2364,8 +2369,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
"status_count": 23,
|
||||
"user_count": 5
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
"contact_account": {
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ func (suite *WrapTestSuite) TestWrapAccountableInUpdate() {
|
|||
"publicKey": {
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
"owner": "http://localhost:8080/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-05-20T11:09:18Z",
|
||||
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package web
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -28,9 +27,24 @@ import (
|
|||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
func (m *Module) profileGETHandler(c *gin.Context) {
|
||||
type profile struct {
|
||||
instance *apimodel.InstanceV1
|
||||
account *apimodel.WebAccount
|
||||
rssFeed string
|
||||
robotsMeta string
|
||||
pinnedStatuses []*apimodel.WebStatus
|
||||
statusResp *apimodel.PageableResponse
|
||||
paging bool
|
||||
}
|
||||
|
||||
// prepareProfile does content type checks, fetches the
|
||||
// targeted account from the db, and converts it to its
|
||||
// web representation, along with other data needed to
|
||||
// render the web view of the account.
|
||||
func (m *Module) prepareProfile(c *gin.Context) *profile {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// We'll need the instance later, and we can also use it
|
||||
|
|
@ -38,7 +52,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
instance, errWithCode := m.processor.InstanceGetV1(ctx)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
|
|
@ -47,90 +61,142 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
return instance, nil
|
||||
}
|
||||
|
||||
// Parse account targetUsername from the URL.
|
||||
targetUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
// Parse + normalize account username from the URL.
|
||||
requestedUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
requestedUsername = strings.ToLower(requestedUsername)
|
||||
|
||||
// Normalize requested username:
|
||||
//
|
||||
// - Usernames on our instance are (currently) always lowercase.
|
||||
//
|
||||
// todo: Update this logic when different username patterns
|
||||
// are allowed, and/or when status slugs are introduced.
|
||||
targetUsername = strings.ToLower(targetUsername)
|
||||
|
||||
// Check what type of content is being requested. If we're getting an AP
|
||||
// request on this endpoint we should render the AP representation instead.
|
||||
accept, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
// Check what type of content is being requested.
|
||||
// If we're getting an AP request on this endpoint
|
||||
// we should render the AP representation instead.
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) {
|
||||
// AP account representation has been requested.
|
||||
m.returnAPAccount(c, targetUsername, accept, instanceGet)
|
||||
return
|
||||
if contentType == string(apiutil.AppActivityJSON) ||
|
||||
contentType == string(apiutil.AppActivityLDJSON) {
|
||||
// AP account representation has
|
||||
// been requested, return that.
|
||||
m.returnAPAccount(c, requestedUsername, contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
// text/html has been requested. Proceed with getting the web view of the account.
|
||||
|
||||
// Fetch the target account so we can do some checks on it.
|
||||
targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername)
|
||||
// text/html has been requested.
|
||||
//
|
||||
// Proceed with getting the web
|
||||
// representation of the account.
|
||||
account, errWithCode := m.processor.Account().GetWeb(ctx, requestedUsername)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// If target account is suspended, this page should not be visible.
|
||||
// If target account is suspended,
|
||||
// this page should not be visible.
|
||||
//
|
||||
// TODO: change this to 410?
|
||||
if targetAccount.Suspended {
|
||||
err := fmt.Errorf("target account %s is suspended", targetUsername)
|
||||
if account.Suspended {
|
||||
err := fmt.Errorf("target account %s is suspended", requestedUsername)
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only generate RSS link if account has RSS enabled.
|
||||
// Only generate RSS link if
|
||||
// account has RSS enabled.
|
||||
var rssFeed string
|
||||
if targetAccount.EnableRSS {
|
||||
rssFeed = "/@" + targetAccount.Username + "/feed.rss"
|
||||
if account.EnableRSS {
|
||||
rssFeed = "/@" + account.Username + "/feed.rss"
|
||||
}
|
||||
|
||||
// Only allow search engines / robots to
|
||||
// index if account is discoverable.
|
||||
// Only allow search robots
|
||||
// if account is discoverable.
|
||||
var robotsMeta string
|
||||
if targetAccount.Discoverable {
|
||||
if account.Discoverable {
|
||||
robotsMeta = apiutil.RobotsDirectivesAllowSome
|
||||
}
|
||||
|
||||
// We need to change our response slightly if the
|
||||
// profile visitor is paging through statuses.
|
||||
// Check if paging.
|
||||
maxStatusID := apiutil.ParseMaxID(c.Query(apiutil.MaxIDKey), "")
|
||||
paging := maxStatusID != ""
|
||||
|
||||
// If not paging, load pinned statuses.
|
||||
var (
|
||||
maxStatusID = apiutil.ParseMaxID(c.Query(apiutil.MaxIDKey), "")
|
||||
paging = maxStatusID != ""
|
||||
mediaOnly = account.WebLayout == "gallery"
|
||||
pinnedStatuses []*apimodel.WebStatus
|
||||
)
|
||||
|
||||
if !paging {
|
||||
// Client opened bare profile (from the top)
|
||||
// so load + display pinned statuses.
|
||||
pinnedStatuses, errWithCode = m.processor.Account().WebStatusesGetPinned(ctx, targetAccount.ID)
|
||||
var errWithCode gtserror.WithCode
|
||||
pinnedStatuses, errWithCode = m.processor.Account().WebStatusesGetPinned(
|
||||
ctx,
|
||||
account.ID,
|
||||
mediaOnly,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get statuses from maxStatusID onwards (or from top if empty string).
|
||||
statusResp, errWithCode := m.processor.Account().WebStatusesGet(ctx, targetAccount.ID, maxStatusID)
|
||||
statusResp, errWithCode := m.processor.Account().WebStatusesGet(
|
||||
ctx,
|
||||
account.ID,
|
||||
mediaOnly,
|
||||
maxStatusID,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &profile{
|
||||
instance: instance,
|
||||
account: account,
|
||||
rssFeed: rssFeed,
|
||||
robotsMeta: robotsMeta,
|
||||
pinnedStatuses: pinnedStatuses,
|
||||
statusResp: statusResp,
|
||||
paging: paging,
|
||||
}
|
||||
}
|
||||
|
||||
// profileGETHandler selects the appropriate rendering
|
||||
// mode for the target account profile, and serves that.
|
||||
func (m *Module) profileGETHandler(c *gin.Context) {
|
||||
p := m.prepareProfile(c)
|
||||
if p == nil {
|
||||
// Something went wrong,
|
||||
// error already written.
|
||||
return
|
||||
}
|
||||
|
||||
// Choose desired web renderer for this acct.
|
||||
switch wrm := p.account.WebLayout; wrm {
|
||||
|
||||
// El classico.
|
||||
case "", "microblog":
|
||||
m.profileMicroblog(c, p)
|
||||
|
||||
// 'gram style media gallery.
|
||||
case "gallery":
|
||||
m.profileGallery(c, p)
|
||||
|
||||
default:
|
||||
log.Panicf(
|
||||
c.Request.Context(),
|
||||
"unknown webrenderingmode %s", wrm,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// profileMicroblog serves the profile
|
||||
// in classic GtS "microblog" view.
|
||||
func (m *Module) profileMicroblog(c *gin.Context, p *profile) {
|
||||
// Prepare stylesheets for profile.
|
||||
stylesheets := make([]string, 0, 7)
|
||||
|
||||
|
|
@ -146,7 +212,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
)
|
||||
|
||||
// User-selected theme if set.
|
||||
if theme := targetAccount.Theme; theme != "" {
|
||||
if theme := p.account.Theme; theme != "" {
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
themesPathPrefix+"/"+theme,
|
||||
|
|
@ -156,23 +222,89 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
// Custom CSS for this user last in cascade.
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
"/@"+targetAccount.Username+"/custom.css",
|
||||
"/@"+p.account.Username+"/custom.css",
|
||||
)
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "profile.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
|
||||
Instance: p.instance,
|
||||
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
||||
Stylesheets: stylesheets,
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{
|
||||
"account": targetAccount,
|
||||
"rssFeed": rssFeed,
|
||||
"robotsMeta": robotsMeta,
|
||||
"statuses": statusResp.Items,
|
||||
"statuses_next": statusResp.NextLink,
|
||||
"pinned_statuses": pinnedStatuses,
|
||||
"show_back_to_top": paging,
|
||||
"account": p.account,
|
||||
"rssFeed": p.rssFeed,
|
||||
"robotsMeta": p.robotsMeta,
|
||||
"statuses": p.statusResp.Items,
|
||||
"statuses_next": p.statusResp.NextLink,
|
||||
"pinned_statuses": p.pinnedStatuses,
|
||||
"show_back_to_top": p.paging,
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
// profileMicroblog serves the profile
|
||||
// in media-only 'gram-style gallery view.
|
||||
func (m *Module) profileGallery(c *gin.Context, p *profile) {
|
||||
// Get just attachments from pinned,
|
||||
// making a rough guess for slice size.
|
||||
pinnedGalleryItems := make([]*apimodel.WebAttachment, 0, len(p.pinnedStatuses)*4)
|
||||
for _, status := range p.pinnedStatuses {
|
||||
pinnedGalleryItems = append(pinnedGalleryItems, status.MediaAttachments...)
|
||||
}
|
||||
|
||||
// Get just attachments from statuses,
|
||||
// making a rough guess for slice size.
|
||||
galleryItems := make([]*apimodel.WebAttachment, 0, len(p.statusResp.Items)*4)
|
||||
for _, statusI := range p.statusResp.Items {
|
||||
status := statusI.(*apimodel.WebStatus)
|
||||
galleryItems = append(galleryItems, status.MediaAttachments...)
|
||||
}
|
||||
|
||||
// Prepare stylesheets for profile.
|
||||
stylesheets := make([]string, 0, 4)
|
||||
|
||||
// Profile gallery stylesheets.
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
[]string{
|
||||
cssFA,
|
||||
cssProfileGallery,
|
||||
}...)
|
||||
|
||||
// User-selected theme if set.
|
||||
if theme := p.account.Theme; theme != "" {
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
themesPathPrefix+"/"+theme,
|
||||
)
|
||||
}
|
||||
|
||||
// Custom CSS for this
|
||||
// user last in cascade.
|
||||
stylesheets = append(
|
||||
stylesheets,
|
||||
"/@"+p.account.Username+"/custom.css",
|
||||
)
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "profile-gallery.tmpl",
|
||||
Instance: p.instance,
|
||||
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
||||
Stylesheets: stylesheets,
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{
|
||||
"account": p.account,
|
||||
"rssFeed": p.rssFeed,
|
||||
"robotsMeta": p.robotsMeta,
|
||||
"pinnedGalleryItems": pinnedGalleryItems,
|
||||
"galleryItems": galleryItems,
|
||||
"statuses": p.statusResp.Items,
|
||||
"statuses_next": p.statusResp.NextLink,
|
||||
"pinned_statuses": p.pinnedStatuses,
|
||||
"show_back_to_top": p.paging,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -184,8 +316,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
func (m *Module) returnAPAccount(
|
||||
c *gin.Context,
|
||||
targetUsername string,
|
||||
accept string,
|
||||
instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode),
|
||||
contentType string,
|
||||
) {
|
||||
user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), targetUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
|
|
@ -193,12 +324,5 @@ func (m *Module) returnAPAccount(
|
|||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("could not marshal json: %w", err)
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, accept, b)
|
||||
apiutil.JSONType(c, http.StatusOK, contentType, user)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,15 +56,16 @@ const (
|
|||
eTagHeader = "ETag" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
||||
lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
|
||||
|
||||
cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
|
||||
cssAbout = distPathPrefix + "/about.css"
|
||||
cssIndex = distPathPrefix + "/index.css"
|
||||
cssLoginInfo = distPathPrefix + "/login-info.css"
|
||||
cssStatus = distPathPrefix + "/status.css"
|
||||
cssThread = distPathPrefix + "/thread.css"
|
||||
cssProfile = distPathPrefix + "/profile.css"
|
||||
cssSettings = distPathPrefix + "/settings-style.css"
|
||||
cssTag = distPathPrefix + "/tag.css"
|
||||
cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
|
||||
cssAbout = distPathPrefix + "/about.css"
|
||||
cssIndex = distPathPrefix + "/index.css"
|
||||
cssLoginInfo = distPathPrefix + "/login-info.css"
|
||||
cssStatus = distPathPrefix + "/status.css"
|
||||
cssThread = distPathPrefix + "/thread.css"
|
||||
cssProfile = distPathPrefix + "/profile.css"
|
||||
cssProfileGallery = distPathPrefix + "/profile-gallery.css"
|
||||
cssSettings = distPathPrefix + "/settings-style.css"
|
||||
cssTag = distPathPrefix + "/tag.css"
|
||||
|
||||
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
|
||||
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue