mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-10 06:54:16 +00:00
[feature/frontend] Add options to include Unlisted posts or hide all posts (#3272)
* [feature/frontend] Add options to include Unlisted posts or hide all posts * finish up * swagger * move invalidate call into bundb package, avoid invalidating if not necessary * rename show_web_statuses => web_visibility * don't use ptr for webvisibility * last bits
This commit is contained in:
parent
7785fa54da
commit
5543fd5340
24 changed files with 523 additions and 161 deletions
|
@ -157,6 +157,14 @@ definitions:
|
||||||
description: The default posting content type for new statuses.
|
description: The default posting content type for new statuses.
|
||||||
type: string
|
type: string
|
||||||
x-go-name: StatusContentType
|
x-go-name: StatusContentType
|
||||||
|
web_visibility:
|
||||||
|
description: |-
|
||||||
|
Visibility level(s) of posts to show for this account via the web api.
|
||||||
|
"public" = default, show only Public visibility posts on the web.
|
||||||
|
"unlisted" = show Public *and* Unlisted visibility posts on the web.
|
||||||
|
"none" = show no posts on the web, not even Public ones.
|
||||||
|
type: string
|
||||||
|
x-go-name: WebVisibility
|
||||||
title: Source represents display or publishing preferences of user's own account.
|
title: Source represents display or publishing preferences of user's own account.
|
||||||
type: object
|
type: object
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
@ -4400,6 +4408,14 @@ paths:
|
||||||
in: formData
|
in: formData
|
||||||
name: hide_collections
|
name: hide_collections
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- description: |-
|
||||||
|
Posts to show on the web view of the account.
|
||||||
|
"public": default, show only Public visibility posts on the web.
|
||||||
|
"unlisted": show Public *and* Unlisted visibility posts on the web.
|
||||||
|
"none": show no posts on the web, not even Public ones.
|
||||||
|
in: formData
|
||||||
|
name: web_visibility
|
||||||
|
type: string
|
||||||
- description: Name of 1st profile field to be added to this account's profile. (The index may be any string; add more indexes to send more fields.)
|
- description: Name of 1st profile field to be added to this account's profile. (The index may be any string; add more indexes to send more fields.)
|
||||||
in: formData
|
in: formData
|
||||||
name: fields_attributes[0][name]
|
name: fields_attributes[0][name]
|
||||||
|
|
|
@ -76,6 +76,26 @@ Some examples:
|
||||||
|
|
||||||
### Visibility and Privacy
|
### Visibility and Privacy
|
||||||
|
|
||||||
|
#### Visibility Level of Posts to Show on Your Profile
|
||||||
|
|
||||||
|
Using this dropdown, you can choose what visibility level(s) of posts should be shown on the public web view of your profile, and served in your RSS feed (if you have enabled RSS).
|
||||||
|
|
||||||
|
**By default, GoToSocial shows only Public visibility posts on the web view of your profile, not Unlisted.** You can adjust this setting to also show Unlisted visibility posts on your profile, which is similar to the default for other ActivityPub softwares like Mastodon etc.
|
||||||
|
|
||||||
|
You can also choose to show no posts at all on the web view of your profile. This allows you to write posts without having to worry about scrapers, rubberneckers, and other nosy parkers visiting your web profile and looking at your posts.
|
||||||
|
|
||||||
|
This setting does not affect visibility of your posts over the ActivityPub protocol, so even if you choose to show no posts on your public web profile, others will be able to see your posts in their client if they follow you, and/or have your posts boosted onto their timeline, use a link to search a post of yours, etc.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Be aware that changes to this setting also apply retroactively.
|
||||||
|
|
||||||
|
That is, if you previously made a post on Unlisted visibility, while set to show only Public posts on your profile, and you change this setting to show Public and Unlisted, then the Unlisted post you previously made will be visible on your profile alongside your Public posts.
|
||||||
|
|
||||||
|
Likewise, if you change this setting to show no posts, then all your posts will be hidden from your profile, regardless of when you created them, and what this option was set to at the time. This will apply until you change this setting again.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Alongside (domain-)blocking, this is a good "emergency" setting to use if you're facing harassment from people trawling through your public posts. It won't hide your posts from people who can see them in their clients, via ActivityPub, but it will at least prevent them from being able to click through your posts in their browser with no authentication, and easily share them with others with a URL.
|
||||||
|
|
||||||
#### Manually Approve Follow Requests (aka Lock Your Account)
|
#### Manually Approve Follow Requests (aka Lock Your Account)
|
||||||
|
|
||||||
This checkbox allows you to decide whether or not you want to manually review follow requests to your account.
|
This checkbox allows you to decide whether or not you want to manually review follow requests to your account.
|
||||||
|
|
|
@ -145,6 +145,15 @@ import (
|
||||||
// description: Hide the account's following/followers collections.
|
// description: Hide the account's following/followers collections.
|
||||||
// type: boolean
|
// type: boolean
|
||||||
// -
|
// -
|
||||||
|
// name: web_visibility
|
||||||
|
// in: formData
|
||||||
|
// description: |-
|
||||||
|
// Posts to show on the web view of the account.
|
||||||
|
// "public": default, show only Public visibility posts on the web.
|
||||||
|
// "unlisted": show Public *and* Unlisted visibility posts on the web.
|
||||||
|
// "none": show no posts on the web, not even Public ones.
|
||||||
|
// type: string
|
||||||
|
// -
|
||||||
// name: fields_attributes[0][name]
|
// name: fields_attributes[0][name]
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: Name of 1st profile field to be added to this account's profile.
|
// description: Name of 1st profile field to be added to this account's profile.
|
||||||
|
@ -339,7 +348,8 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
|
||||||
form.Theme == nil &&
|
form.Theme == nil &&
|
||||||
form.CustomCSS == nil &&
|
form.CustomCSS == nil &&
|
||||||
form.EnableRSS == nil &&
|
form.EnableRSS == nil &&
|
||||||
form.HideCollections == nil) {
|
form.HideCollections == nil &&
|
||||||
|
form.WebVisibility == nil) {
|
||||||
return nil, errors.New("empty form submitted")
|
return nil, errors.New("empty form submitted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,6 +227,9 @@ type UpdateCredentialsRequest struct {
|
||||||
EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
|
EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
|
||||||
// Hide this account's following/followers collections.
|
// Hide this account's following/followers collections.
|
||||||
HideCollections *bool `form:"hide_collections" json:"hide_collections"`
|
HideCollections *bool `form:"hide_collections" json:"hide_collections"`
|
||||||
|
// 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
||||||
|
|
|
@ -26,6 +26,11 @@ type Source struct {
|
||||||
// private = Followers-only post
|
// private = Followers-only post
|
||||||
// direct = Direct post
|
// direct = Direct post
|
||||||
Privacy Visibility `json:"privacy"`
|
Privacy Visibility `json:"privacy"`
|
||||||
|
// Visibility level(s) of posts to show for this account via the web api.
|
||||||
|
// "public" = default, show only Public visibility posts on the web.
|
||||||
|
// "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"`
|
||||||
// Whether new statuses should be marked sensitive by default.
|
// Whether new statuses should be marked sensitive by default.
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
// The default posting language for new statuses.
|
// The default posting language for new statuses.
|
||||||
|
|
|
@ -232,6 +232,8 @@ type StatusCreateRequest struct {
|
||||||
type Visibility string
|
type Visibility string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// VisibilityNone is visible to nobody. This is only used for the visibility of web statuses.
|
||||||
|
VisibilityNone Visibility = "none"
|
||||||
// VisibilityPublic is visible to everyone, and will be available via the web even for nonauthenticated users.
|
// VisibilityPublic is visible to everyone, and will be available via the web even for nonauthenticated users.
|
||||||
VisibilityPublic Visibility = "public"
|
VisibilityPublic Visibility = "public"
|
||||||
// VisibilityUnlisted is visible to everyone, but only on home timelines, lists, etc.
|
// VisibilityUnlisted is visible to everyone, but only on home timelines, lists, etc.
|
||||||
|
|
|
@ -117,12 +117,11 @@ type Account interface {
|
||||||
// In the case of no statuses, this function will return db.ErrNoEntries.
|
// In the case of no statuses, this function will return db.ErrNoEntries.
|
||||||
GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, error)
|
GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, error)
|
||||||
|
|
||||||
// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for returning statuses that
|
// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for
|
||||||
// should be visible via the web view of an account. So, only public, federated statuses that aren't boosts
|
// returning statuses that should be visible via the web view of a *LOCAL* account.
|
||||||
// or replies.
|
|
||||||
//
|
//
|
||||||
// In the case of no statuses, this function will return db.ErrNoEntries.
|
// In the case of no statuses, this function will return db.ErrNoEntries.
|
||||||
GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, error)
|
GetAccountWebStatuses(ctx context.Context, account *gtsmodel.Account, limit int, maxID string) ([]*gtsmodel.Status, error)
|
||||||
|
|
||||||
// SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment.
|
// SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment.
|
||||||
SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachment *gtsmodel.MediaAttachment, accountID string) error
|
SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachment *gtsmodel.MediaAttachment, accountID string) error
|
||||||
|
|
|
@ -1047,7 +1047,18 @@ func (a *accountDB) GetAccountPinnedStatuses(ctx context.Context, accountID stri
|
||||||
return a.state.DB.GetStatusesByIDs(ctx, statusIDs)
|
return a.state.DB.GetStatusesByIDs(ctx, statusIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, error) {
|
func (a *accountDB) GetAccountWebStatuses(
|
||||||
|
ctx context.Context,
|
||||||
|
account *gtsmodel.Account,
|
||||||
|
limit int,
|
||||||
|
maxID string,
|
||||||
|
) ([]*gtsmodel.Status, error) {
|
||||||
|
// Check for an easy case: account exposes no statuses via the web.
|
||||||
|
webVisibility := account.Settings.WebVisibility
|
||||||
|
if webVisibility == gtsmodel.VisibilityNone {
|
||||||
|
return nil, db.ErrNoEntries
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure reasonable
|
// Ensure reasonable
|
||||||
if limit < 0 {
|
if limit < 0 {
|
||||||
limit = 0
|
limit = 0
|
||||||
|
@ -1061,14 +1072,36 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
|
||||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||||
// Select only IDs from table
|
// Select only IDs from table
|
||||||
Column("status.id").
|
Column("status.id").
|
||||||
Where("? = ?", bun.Ident("status.account_id"), accountID).
|
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||||
// Don't show replies or boosts.
|
// Don't show replies or boosts.
|
||||||
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
|
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
|
||||||
Where("? IS NULL", bun.Ident("status.boost_of_id")).
|
Where("? IS NULL", bun.Ident("status.boost_of_id"))
|
||||||
|
|
||||||
|
// Select statuses for this account according
|
||||||
|
// to their web visibility preference.
|
||||||
|
switch webVisibility {
|
||||||
|
|
||||||
|
case gtsmodel.VisibilityPublic:
|
||||||
// Only Public statuses.
|
// Only Public statuses.
|
||||||
Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic).
|
q = q.Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic)
|
||||||
// Don't show local-only statuses on the web view.
|
|
||||||
Where("? = ?", bun.Ident("status.federated"), true)
|
case gtsmodel.VisibilityUnlocked:
|
||||||
|
// Public or Unlocked.
|
||||||
|
visis := []gtsmodel.Visibility{
|
||||||
|
gtsmodel.VisibilityPublic,
|
||||||
|
gtsmodel.VisibilityUnlocked,
|
||||||
|
}
|
||||||
|
q = q.Where("? IN (?)", bun.Ident("status.visibility"), bun.In(visis))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, gtserror.Newf(
|
||||||
|
"unrecognized web visibility for account %s: %s",
|
||||||
|
account.ID, webVisibility,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show local-only statuses on the web view.
|
||||||
|
q = q.Where("? = ?", bun.Ident("status.federated"), true)
|
||||||
|
|
||||||
// return only statuses LOWER (ie., older) than maxID
|
// return only statuses LOWER (ie., older) than maxID
|
||||||
if maxID == "" {
|
if maxID == "" {
|
||||||
|
@ -1145,10 +1178,30 @@ func (a *accountDB) UpdateAccountSettings(
|
||||||
) error {
|
) error {
|
||||||
return a.state.Caches.DB.AccountSettings.Store(settings, func() error {
|
return a.state.Caches.DB.AccountSettings.Store(settings, func() error {
|
||||||
settings.UpdatedAt = time.Now()
|
settings.UpdatedAt = time.Now()
|
||||||
if len(columns) > 0 {
|
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case len(columns) != 0:
|
||||||
// If we're updating by column,
|
// If we're updating by column,
|
||||||
// ensure "updated_at" is included.
|
// ensure "updated_at" is included.
|
||||||
columns = append(columns, "updated_at")
|
columns = append(columns, "updated_at")
|
||||||
|
|
||||||
|
// If we're updating web_visibility we should
|
||||||
|
// fall through + invalidate visibility cache.
|
||||||
|
if !slices.Contains(columns, "web_visibility") {
|
||||||
|
break // No need to invalidate.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallthrough
|
||||||
|
// to invalidate.
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case len(columns) == 0:
|
||||||
|
// Status visibility may be changing for this account.
|
||||||
|
// Clear the visibility cache for unauthed requesters.
|
||||||
|
//
|
||||||
|
// todo: invalidate JUST this account's statuses.
|
||||||
|
defer a.state.Caches.Visibility.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := a.db.
|
if _, err := a.db.
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
// 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/gtsmodel"
|
||||||
|
"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 {
|
||||||
|
|
||||||
|
// If column already exists we don't need to do anything.
|
||||||
|
exists, err := doesColumnExist(ctx, tx,
|
||||||
|
"account_settings", "web_visibility",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Real error.
|
||||||
|
return err
|
||||||
|
} else if exists {
|
||||||
|
// Nothing to do.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new column.
|
||||||
|
if _, err := tx.NewAddColumn().
|
||||||
|
Table("account_settings").
|
||||||
|
ColumnExpr(
|
||||||
|
"? TEXT NOT NULL DEFAULT ?",
|
||||||
|
bun.Ident("web_visibility"),
|
||||||
|
gtsmodel.VisibilityPublic,
|
||||||
|
).
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
down := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Migrations.Register(up, down); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ func (f *Filter) AccountVisible(ctx context.Context, requester *gtsmodel.Account
|
||||||
const vtype = cache.VisibilityTypeAccount
|
const vtype = cache.VisibilityTypeAccount
|
||||||
|
|
||||||
// By default we assume no auth.
|
// By default we assume no auth.
|
||||||
requesterID := noauth
|
requesterID := NoAuth
|
||||||
|
|
||||||
if requester != nil {
|
if requester != nil {
|
||||||
// Use provided account ID.
|
// Use provided account ID.
|
||||||
|
|
|
@ -21,9 +21,9 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// noauth is a placeholder ID used in cache lookups
|
// NoAuth is a placeholder ID used in cache lookups
|
||||||
// when there is no authorized account ID to use.
|
// when there is no authorized account ID to use.
|
||||||
const noauth = "noauth"
|
const NoAuth = "noauth"
|
||||||
|
|
||||||
// Filter packages up a bunch of logic for checking whether
|
// Filter packages up a bunch of logic for checking whether
|
||||||
// given statuses or accounts are visible to a requester.
|
// given statuses or accounts are visible to a requester.
|
||||||
|
|
|
@ -35,7 +35,7 @@ func (f *Filter) StatusHomeTimelineable(ctx context.Context, owner *gtsmodel.Acc
|
||||||
const vtype = cache.VisibilityTypeHome
|
const vtype = cache.VisibilityTypeHome
|
||||||
|
|
||||||
// By default we assume no auth.
|
// By default we assume no auth.
|
||||||
requesterID := noauth
|
requesterID := NoAuth
|
||||||
|
|
||||||
if owner != nil {
|
if owner != nil {
|
||||||
// Use provided account ID.
|
// Use provided account ID.
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (f *Filter) StatusPublicTimelineable(ctx context.Context, requester *gtsmod
|
||||||
const vtype = cache.VisibilityTypePublic
|
const vtype = cache.VisibilityTypePublic
|
||||||
|
|
||||||
// By default we assume no auth.
|
// By default we assume no auth.
|
||||||
requesterID := noauth
|
requesterID := NoAuth
|
||||||
|
|
||||||
if requester != nil {
|
if requester != nil {
|
||||||
// Use provided account ID.
|
// Use provided account ID.
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (f *Filter) StatusVisible(
|
||||||
const vtype = cache.VisibilityTypeStatus
|
const vtype = cache.VisibilityTypeStatus
|
||||||
|
|
||||||
// By default we assume no auth.
|
// By default we assume no auth.
|
||||||
requesterID := noauth
|
requesterID := NoAuth
|
||||||
|
|
||||||
if requester != nil {
|
if requester != nil {
|
||||||
// Use provided account ID.
|
// Use provided account ID.
|
||||||
|
@ -113,9 +113,9 @@ func (f *Filter) isStatusVisible(
|
||||||
}
|
}
|
||||||
|
|
||||||
if requester == nil {
|
if requester == nil {
|
||||||
// The request is unauthed. Only federated, Public statuses are visible without auth.
|
// Use a different visibility
|
||||||
visibleUnauthed := !status.IsLocalOnly() && status.Visibility == gtsmodel.VisibilityPublic
|
// heuristic for unauthed requests.
|
||||||
return visibleUnauthed, nil
|
return f.isStatusVisibleUnauthed(ctx, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -245,6 +245,62 @@ func (f *Filter) isPendingStatusVisible(
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Filter) isStatusVisibleUnauthed(
|
||||||
|
ctx context.Context,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) (bool, error) {
|
||||||
|
// For remote accounts, only show
|
||||||
|
// Public statuses via the web.
|
||||||
|
if status.Account.IsRemote() {
|
||||||
|
return status.Visibility == gtsmodel.VisibilityPublic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status is local only,
|
||||||
|
// never show via the web.
|
||||||
|
if status.IsLocalOnly() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check account's settings to see
|
||||||
|
// what they expose. Populate these
|
||||||
|
// from the DB if necessary.
|
||||||
|
if status.Account.Settings == nil {
|
||||||
|
var err error
|
||||||
|
status.Account.Settings, err = f.state.DB.GetAccountSettings(ctx, status.Account.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, gtserror.Newf(
|
||||||
|
"error getting settings for account %s: %w",
|
||||||
|
status.Account.ID, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webVisibility := status.Account.Settings.WebVisibility
|
||||||
|
switch webVisibility {
|
||||||
|
|
||||||
|
// public_only: status must be Public.
|
||||||
|
case gtsmodel.VisibilityPublic:
|
||||||
|
return status.Visibility == gtsmodel.VisibilityPublic, nil
|
||||||
|
|
||||||
|
// unlisted: status must be Public or Unlocked.
|
||||||
|
case gtsmodel.VisibilityUnlocked:
|
||||||
|
visible := status.Visibility == gtsmodel.VisibilityPublic ||
|
||||||
|
status.Visibility == gtsmodel.VisibilityUnlocked
|
||||||
|
return visible, nil
|
||||||
|
|
||||||
|
// none: never show via the web.
|
||||||
|
case gtsmodel.VisibilityNone:
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
// Huh?
|
||||||
|
default:
|
||||||
|
return false, gtserror.Newf(
|
||||||
|
"unrecognized web visibility for account %s: %s",
|
||||||
|
status.Account.ID, webVisibility,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// areStatusAccountsVisible calls Filter{}.AccountVisible() on status author and the status boost-of (if set) author, returning visibility of status (and boost-of) to requester.
|
// areStatusAccountsVisible calls Filter{}.AccountVisible() on status author and the status boost-of (if set) author, returning visibility of status (and boost-of) to requester.
|
||||||
func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
||||||
// Check whether status author's account is visible to requester.
|
// Check whether status author's account is visible to requester.
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
|
|
||||||
package gtsmodel
|
package gtsmodel
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// AccountSettings models settings / preferences for a local, non-instance account.
|
// AccountSettings models settings / preferences for a local, non-instance account.
|
||||||
type AccountSettings struct {
|
type AccountSettings struct {
|
||||||
|
@ -32,6 +34,7 @@ type AccountSettings struct {
|
||||||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
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.
|
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||||
|
WebVisibility Visibility `bun:",nullzero,notnull,default:public"` // Visibility level of statuses that visitors can view via the web profile.
|
||||||
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
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.
|
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.
|
InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
|
||||||
|
|
|
@ -238,6 +238,9 @@ type StatusToEmoji struct {
|
||||||
type Visibility string
|
type Visibility string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// VisibilityNone means nobody can see this.
|
||||||
|
// It's only used for web status visibility.
|
||||||
|
VisibilityNone Visibility = "none"
|
||||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||||
VisibilityPublic Visibility = "public"
|
VisibilityPublic Visibility = "public"
|
||||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
|
||||||
feed.Updated = lastPostAt
|
feed.Updated = lastPostAt
|
||||||
|
|
||||||
// Retrieve latest statuses as they'd be shown on the web view of the account profile.
|
// Retrieve latest statuses as they'd be shown on the web view of the account profile.
|
||||||
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account.ID, rssFeedLength, "")
|
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, rssFeedLength, "")
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
err = fmt.Errorf("db error getting account web statuses: %w", err)
|
err = fmt.Errorf("db error getting account web statuses: %w", err)
|
||||||
return "", gtserror.NewErrorInternalError(err)
|
return "", gtserror.NewErrorInternalError(err)
|
||||||
|
|
|
@ -159,7 +159,7 @@ func (p *Processor) WebStatusesGet(
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID)
|
statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, 10, maxID)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
@ -206,9 +206,15 @@ func (p *Processor) WebStatusesGetPinned(
|
||||||
|
|
||||||
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
|
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
|
||||||
for _, status := range statuses {
|
for _, status := range statuses {
|
||||||
if status.Visibility != gtsmodel.VisibilityPublic {
|
// Ensure visible via the web.
|
||||||
// Skip non-public
|
visible, err := p.visFilter.StatusVisible(ctx, nil, status)
|
||||||
// pinned status.
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "error checking status visibility: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
// Don't serve.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,21 +54,44 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
|
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Indicates that the account's
|
||||||
|
// note, display name, and/or fields
|
||||||
|
// have changed, and so emojis should
|
||||||
|
// be re-parsed and updated as well.
|
||||||
|
textChanged bool
|
||||||
|
|
||||||
|
// DB columns on the account
|
||||||
|
// that need to be updated.
|
||||||
|
acctColumns []string
|
||||||
|
|
||||||
|
// DB columns on the settings
|
||||||
|
// that need to be updated.
|
||||||
|
settingsColumns []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Account flags.
|
||||||
|
|
||||||
if form.Discoverable != nil {
|
if form.Discoverable != nil {
|
||||||
account.Discoverable = form.Discoverable
|
account.Discoverable = form.Discoverable
|
||||||
|
acctColumns = append(acctColumns, "discoverable")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Bot != nil {
|
if form.Bot != nil {
|
||||||
account.Bot = form.Bot
|
account.Bot = form.Bot
|
||||||
|
acctColumns = append(acctColumns, "bot")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Via the process of updating the account,
|
if form.Locked != nil {
|
||||||
// it is possible that the emojis used by
|
account.Locked = form.Locked
|
||||||
// that account in note/display name/fields
|
acctColumns = append(acctColumns, "locked")
|
||||||
// may change; we need to keep track of this.
|
}
|
||||||
var emojisChanged bool
|
|
||||||
|
|
||||||
if form.DisplayName != nil {
|
if form.DisplayName != nil {
|
||||||
|
// Display name text
|
||||||
|
// is changing.
|
||||||
|
textChanged = true
|
||||||
|
|
||||||
displayName := *form.DisplayName
|
displayName := *form.DisplayName
|
||||||
if err := validate.DisplayName(displayName); err != nil {
|
if err := validate.DisplayName(displayName); err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
@ -76,137 +99,54 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
|
|
||||||
// Parse new display name (always from plaintext).
|
// Parse new display name (always from plaintext).
|
||||||
account.DisplayName = text.SanitizeToPlaintext(displayName)
|
account.DisplayName = text.SanitizeToPlaintext(displayName)
|
||||||
|
acctColumns = append(acctColumns, "display_name")
|
||||||
// If display name has changed, account emojis may have also changed.
|
|
||||||
emojisChanged = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Note != nil {
|
if form.Note != nil {
|
||||||
|
// Note text is changing.
|
||||||
|
textChanged = true
|
||||||
|
|
||||||
note := *form.Note
|
note := *form.Note
|
||||||
if err := validate.Note(note); err != nil {
|
if err := validate.Note(note); err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store raw version of the note for now,
|
// Store raw version of note
|
||||||
// we'll process the proper version later.
|
// for now, we'll process
|
||||||
|
// the proper version later.
|
||||||
account.NoteRaw = note
|
account.NoteRaw = note
|
||||||
|
acctColumns = append(acctColumns, []string{
|
||||||
// If note has changed, account emojis may have also changed.
|
"note",
|
||||||
emojisChanged = true
|
"note_raw",
|
||||||
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.FieldsAttributes != nil {
|
if form.FieldsAttributes != nil {
|
||||||
var (
|
// Field text is changing.
|
||||||
fieldsAttributes = *form.FieldsAttributes
|
textChanged = true
|
||||||
fieldsLen = len(fieldsAttributes)
|
|
||||||
fieldsRaw = make([]*gtsmodel.Field, 0, fieldsLen)
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, updateField := range fieldsAttributes {
|
if err := p.updateFields(
|
||||||
if updateField.Name == nil || updateField.Value == nil {
|
account,
|
||||||
continue
|
*form.FieldsAttributes,
|
||||||
}
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
var (
|
|
||||||
name string = *updateField.Name
|
|
||||||
value string = *updateField.Value
|
|
||||||
)
|
|
||||||
|
|
||||||
if name == "" || value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize raw field values.
|
|
||||||
fieldRaw := >smodel.Field{
|
|
||||||
Name: text.SanitizeToPlaintext(name),
|
|
||||||
Value: text.SanitizeToPlaintext(value),
|
|
||||||
}
|
|
||||||
fieldsRaw = append(fieldsRaw, fieldRaw)
|
|
||||||
}
|
}
|
||||||
|
acctColumns = append(acctColumns, []string{
|
||||||
// Check length of parsed raw fields.
|
"fields",
|
||||||
if err := validate.ProfileFields(fieldsRaw); err != nil {
|
"fields_raw",
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
}...)
|
||||||
}
|
|
||||||
|
|
||||||
// OK, new raw fields are valid.
|
|
||||||
account.FieldsRaw = fieldsRaw
|
|
||||||
account.Fields = make([]*gtsmodel.Field, 0, fieldsLen) // process these in a sec
|
|
||||||
|
|
||||||
// If fields have changed, account emojis may also have changed.
|
|
||||||
emojisChanged = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if emojisChanged {
|
if textChanged {
|
||||||
// Use map to deduplicate emojis by their ID.
|
// Process display name, note, fields,
|
||||||
emojis := make(map[string]*gtsmodel.Emoji)
|
// and any concomitant emoji changes.
|
||||||
|
p.processAccountText(ctx, account)
|
||||||
// Retrieve display name emojis.
|
acctColumns = append(acctColumns, "emojis")
|
||||||
for _, emoji := range p.formatter.FromPlainEmojiOnly(
|
|
||||||
ctx,
|
|
||||||
p.parseMention,
|
|
||||||
account.ID,
|
|
||||||
"",
|
|
||||||
account.DisplayName,
|
|
||||||
).Emojis {
|
|
||||||
emojis[emoji.ID] = emoji
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format + set note according to user prefs.
|
|
||||||
f := p.selectNoteFormatter(account.Settings.StatusContentType)
|
|
||||||
formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
|
|
||||||
account.Note = formatNoteResult.HTML
|
|
||||||
|
|
||||||
// Retrieve note emojis.
|
|
||||||
for _, emoji := range formatNoteResult.Emojis {
|
|
||||||
emojis[emoji.ID] = emoji
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the raw fields we stored earlier.
|
|
||||||
account.Fields = make([]*gtsmodel.Field, 0, len(account.FieldsRaw))
|
|
||||||
for _, fieldRaw := range account.FieldsRaw {
|
|
||||||
field := >smodel.Field{}
|
|
||||||
|
|
||||||
// Name stays plain, but we still need to
|
|
||||||
// see if there are any emojis set in it.
|
|
||||||
field.Name = fieldRaw.Name
|
|
||||||
for _, emoji := range p.formatter.FromPlainEmojiOnly(
|
|
||||||
ctx,
|
|
||||||
p.parseMention,
|
|
||||||
account.ID,
|
|
||||||
"",
|
|
||||||
fieldRaw.Name,
|
|
||||||
).Emojis {
|
|
||||||
emojis[emoji.ID] = emoji
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value can be HTML, but we don't want
|
|
||||||
// to wrap the result in <p> tags.
|
|
||||||
fieldFormatValueResult := p.formatter.FromPlainNoParagraph(ctx, p.parseMention, account.ID, "", fieldRaw.Value)
|
|
||||||
field.Value = fieldFormatValueResult.HTML
|
|
||||||
|
|
||||||
// Retrieve field emojis.
|
|
||||||
for _, emoji := range fieldFormatValueResult.Emojis {
|
|
||||||
emojis[emoji.ID] = emoji
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're done, append the shiny new field.
|
|
||||||
account.Fields = append(account.Fields, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
emojisCount := len(emojis)
|
|
||||||
account.Emojis = make([]*gtsmodel.Emoji, 0, emojisCount)
|
|
||||||
account.EmojiIDs = make([]string, 0, emojisCount)
|
|
||||||
|
|
||||||
for id, emoji := range emojis {
|
|
||||||
account.Emojis = append(account.Emojis, emoji)
|
|
||||||
account.EmojiIDs = append(account.EmojiIDs, id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.AvatarDescription != nil {
|
if form.AvatarDescription != nil {
|
||||||
desc := text.SanitizeToPlaintext(*form.AvatarDescription)
|
desc := text.SanitizeToPlaintext(*form.AvatarDescription)
|
||||||
form.AvatarDescription = util.Ptr(desc)
|
form.AvatarDescription = &desc
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||||
|
@ -220,7 +160,7 @@ 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)
|
acctColumns = append(acctColumns, "avatar_media_attachment_id")
|
||||||
} else if form.AvatarDescription != nil && account.AvatarMediaAttachment != nil {
|
} else if form.AvatarDescription != nil && account.AvatarMediaAttachment != nil {
|
||||||
// Update just existing description if possible.
|
// Update just existing description if possible.
|
||||||
account.AvatarMediaAttachment.Description = *form.AvatarDescription
|
account.AvatarMediaAttachment.Description = *form.AvatarDescription
|
||||||
|
@ -250,7 +190,7 @@ 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)
|
acctColumns = append(acctColumns, "header_media_attachment_id")
|
||||||
} else if form.HeaderDescription != nil && account.HeaderMediaAttachment != nil {
|
} else if form.HeaderDescription != nil && account.HeaderMediaAttachment != nil {
|
||||||
// Update just existing description if possible.
|
// Update just existing description if possible.
|
||||||
account.HeaderMediaAttachment.Description = *form.HeaderDescription
|
account.HeaderMediaAttachment.Description = *form.HeaderDescription
|
||||||
|
@ -264,29 +204,32 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Locked != nil {
|
// Account settings flags.
|
||||||
account.Locked = form.Locked
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Source != nil {
|
if form.Source != nil {
|
||||||
if form.Source.Language != nil {
|
if form.Source.Language != nil {
|
||||||
language, err := validate.Language(*form.Source.Language)
|
language, err := validate.Language(*form.Source.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
account.Settings.Language = language
|
account.Settings.Language = language
|
||||||
|
settingsColumns = append(settingsColumns, "language")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Source.Sensitive != nil {
|
if form.Source.Sensitive != nil {
|
||||||
account.Settings.Sensitive = form.Source.Sensitive
|
account.Settings.Sensitive = form.Source.Sensitive
|
||||||
|
settingsColumns = append(settingsColumns, "sensitive")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Source.Privacy != nil {
|
if form.Source.Privacy != nil {
|
||||||
if err := validate.Privacy(*form.Source.Privacy); err != nil {
|
if err := validate.Privacy(*form.Source.Privacy); err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
|
|
||||||
account.Settings.Privacy = privacy
|
priv := apimodel.Visibility(*form.Source.Privacy)
|
||||||
|
account.Settings.Privacy = typeutils.APIVisToVis(priv)
|
||||||
|
settingsColumns = append(settingsColumns, "privacy")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Source.StatusContentType != nil {
|
if form.Source.StatusContentType != nil {
|
||||||
|
@ -295,6 +238,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
|
|
||||||
account.Settings.StatusContentType = *form.Source.StatusContentType
|
account.Settings.StatusContentType = *form.Source.StatusContentType
|
||||||
|
settingsColumns = append(settingsColumns, "status_content_type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,6 +256,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
account.Settings.Theme = theme
|
account.Settings.Theme = theme
|
||||||
}
|
}
|
||||||
|
settingsColumns = append(settingsColumns, "theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.CustomCSS != nil {
|
if form.CustomCSS != nil {
|
||||||
|
@ -319,25 +264,54 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
if err := validate.CustomCSS(customCSS); err != nil {
|
if err := validate.CustomCSS(customCSS); err != nil {
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS)
|
account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS)
|
||||||
|
settingsColumns = append(settingsColumns, "custom_css")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.EnableRSS != nil {
|
if form.EnableRSS != nil {
|
||||||
account.Settings.EnableRSS = form.EnableRSS
|
account.Settings.EnableRSS = form.EnableRSS
|
||||||
|
settingsColumns = append(settingsColumns, "enable_rss")
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.HideCollections != nil {
|
if form.HideCollections != nil {
|
||||||
account.Settings.HideCollections = form.HideCollections
|
account.Settings.HideCollections = form.HideCollections
|
||||||
|
settingsColumns = append(settingsColumns, "hide_collections")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.state.DB.UpdateAccount(ctx, account); err != nil {
|
if form.WebVisibility != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
|
apiVis := apimodel.Visibility(*form.WebVisibility)
|
||||||
|
webVisibility := typeutils.APIVisToVis(apiVis)
|
||||||
|
if webVisibility != gtsmodel.VisibilityPublic &&
|
||||||
|
webVisibility != gtsmodel.VisibilityUnlocked &&
|
||||||
|
webVisibility != gtsmodel.VisibilityNone {
|
||||||
|
const text = "web_visibility must be one of public, unlocked, or none"
|
||||||
|
err := errors.New(text)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Settings.WebVisibility = webVisibility
|
||||||
|
settingsColumns = append(settingsColumns, "web_visibility")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.state.DB.UpdateAccountSettings(ctx, account.Settings); err != nil {
|
// We've parsed + set everything, do
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account settings %s: %s", account.ID, err))
|
// necessary database updates now.
|
||||||
|
|
||||||
|
if len(acctColumns) > 0 {
|
||||||
|
if err := p.state.DB.UpdateAccount(ctx, account, acctColumns...); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating account %s: %w", account.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(settingsColumns) > 0 {
|
||||||
|
if err := p.state.DB.UpdateAccountSettings(ctx, account.Settings, settingsColumns...); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating account settings %s: %w", account.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send out Update message over the s2s (fedi) API.
|
||||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||||
APObjectType: ap.ActorPerson,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityUpdate,
|
APActivityType: ap.ActivityUpdate,
|
||||||
|
@ -347,11 +321,133 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
|
|
||||||
acctSensitive, err := p.converter.AccountToAPIAccountSensitive(ctx, account)
|
acctSensitive, err := p.converter.AccountToAPIAccountSensitive(ctx, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not convert account into apisensitive account: %s", err))
|
err := gtserror.Newf("error converting account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return acctSensitive, nil
|
return acctSensitive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateFields sets FieldsRaw on the given
|
||||||
|
// account, and resets account.Fields to an
|
||||||
|
// empty slice, ready for further processing.
|
||||||
|
func (p *Processor) updateFields(
|
||||||
|
account *gtsmodel.Account,
|
||||||
|
fieldsAttributes []apimodel.UpdateField,
|
||||||
|
) gtserror.WithCode {
|
||||||
|
var (
|
||||||
|
fieldsLen = len(fieldsAttributes)
|
||||||
|
fieldsRaw = make([]*gtsmodel.Field, 0, fieldsLen)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, updateField := range fieldsAttributes {
|
||||||
|
if updateField.Name == nil || updateField.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
name string = *updateField.Name
|
||||||
|
value string = *updateField.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
if name == "" || value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize raw field values.
|
||||||
|
fieldRaw := >smodel.Field{
|
||||||
|
Name: text.SanitizeToPlaintext(name),
|
||||||
|
Value: text.SanitizeToPlaintext(value),
|
||||||
|
}
|
||||||
|
fieldsRaw = append(fieldsRaw, fieldRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check length of parsed raw fields.
|
||||||
|
if err := validate.ProfileFields(fieldsRaw); err != nil {
|
||||||
|
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK, new raw fields are valid.
|
||||||
|
account.FieldsRaw = fieldsRaw
|
||||||
|
account.Fields = make([]*gtsmodel.Field, 0, fieldsLen)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAccountText processes the raw versions of the given
|
||||||
|
// account's display name, note, and fields, and sets those
|
||||||
|
// processed versions on the account, while also updating the
|
||||||
|
// account's emojis entry based on the results of the processing.
|
||||||
|
func (p *Processor) processAccountText(
|
||||||
|
ctx context.Context,
|
||||||
|
account *gtsmodel.Account,
|
||||||
|
) {
|
||||||
|
// Use map to deduplicate emojis by their ID.
|
||||||
|
emojis := make(map[string]*gtsmodel.Emoji)
|
||||||
|
|
||||||
|
// Retrieve display name emojis.
|
||||||
|
for _, emoji := range p.formatter.FromPlainEmojiOnly(
|
||||||
|
ctx,
|
||||||
|
p.parseMention,
|
||||||
|
account.ID,
|
||||||
|
"",
|
||||||
|
account.DisplayName,
|
||||||
|
).Emojis {
|
||||||
|
emojis[emoji.ID] = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format + set note according to user prefs.
|
||||||
|
f := p.selectNoteFormatter(account.Settings.StatusContentType)
|
||||||
|
formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
|
||||||
|
account.Note = formatNoteResult.HTML
|
||||||
|
|
||||||
|
// Retrieve note emojis.
|
||||||
|
for _, emoji := range formatNoteResult.Emojis {
|
||||||
|
emojis[emoji.ID] = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process raw fields.
|
||||||
|
account.Fields = make([]*gtsmodel.Field, 0, len(account.FieldsRaw))
|
||||||
|
for _, fieldRaw := range account.FieldsRaw {
|
||||||
|
field := >smodel.Field{}
|
||||||
|
|
||||||
|
// Name stays plain, but we still need to
|
||||||
|
// see if there are any emojis set in it.
|
||||||
|
field.Name = fieldRaw.Name
|
||||||
|
for _, emoji := range p.formatter.FromPlainEmojiOnly(
|
||||||
|
ctx,
|
||||||
|
p.parseMention,
|
||||||
|
account.ID,
|
||||||
|
"",
|
||||||
|
fieldRaw.Name,
|
||||||
|
).Emojis {
|
||||||
|
emojis[emoji.ID] = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value can be HTML, but we don't want
|
||||||
|
// to wrap the result in <p> tags.
|
||||||
|
fieldFormatValueResult := p.formatter.FromPlainNoParagraph(ctx, p.parseMention, account.ID, "", fieldRaw.Value)
|
||||||
|
field.Value = fieldFormatValueResult.HTML
|
||||||
|
|
||||||
|
// Retrieve field emojis.
|
||||||
|
for _, emoji := range fieldFormatValueResult.Emojis {
|
||||||
|
emojis[emoji.ID] = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done, append the shiny new field.
|
||||||
|
account.Fields = append(account.Fields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the account's emojis.
|
||||||
|
emojisCount := len(emojis)
|
||||||
|
account.Emojis = make([]*gtsmodel.Emoji, 0, emojisCount)
|
||||||
|
account.EmojiIDs = make([]string, 0, emojisCount)
|
||||||
|
|
||||||
|
for id, emoji := range emojis {
|
||||||
|
account.Emojis = append(account.Emojis, emoji)
|
||||||
|
account.EmojiIDs = append(account.EmojiIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateAvatar does the dirty work of checking the avatar
|
// UpdateAvatar does the dirty work of checking the avatar
|
||||||
// part of an account update form, parsing and checking the
|
// part of an account update form, parsing and checking the
|
||||||
// media, and doing the necessary updates in the database
|
// media, and doing the necessary updates in the database
|
||||||
|
|
|
@ -38,6 +38,8 @@ func APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility {
|
||||||
return gtsmodel.VisibilityMutualsOnly
|
return gtsmodel.VisibilityMutualsOnly
|
||||||
case apimodel.VisibilityDirect:
|
case apimodel.VisibilityDirect:
|
||||||
return gtsmodel.VisibilityDirect
|
return gtsmodel.VisibilityDirect
|
||||||
|
case apimodel.VisibilityNone:
|
||||||
|
return gtsmodel.VisibilityNone
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,7 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
||||||
|
|
||||||
apiAccount.Source = &apimodel.Source{
|
apiAccount.Source = &apimodel.Source{
|
||||||
Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy),
|
Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy),
|
||||||
|
WebVisibility: c.VisToAPIVis(ctx, a.Settings.WebVisibility),
|
||||||
Sensitive: *a.Settings.Sensitive,
|
Sensitive: *a.Settings.Sensitive,
|
||||||
Language: a.Settings.Language,
|
Language: a.Settings.Language,
|
||||||
StatusContentType: statusContentType,
|
StatusContentType: statusContentType,
|
||||||
|
|
|
@ -120,6 +120,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
||||||
"fields": [],
|
"fields": [],
|
||||||
"source": {
|
"source": {
|
||||||
"privacy": "public",
|
"privacy": "public",
|
||||||
|
"web_visibility": "unlisted",
|
||||||
"sensitive": false,
|
"sensitive": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"status_content_type": "text/plain",
|
"status_content_type": "text/plain",
|
||||||
|
@ -304,6 +305,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
||||||
"fields": [],
|
"fields": [],
|
||||||
"source": {
|
"source": {
|
||||||
"privacy": "public",
|
"privacy": "public",
|
||||||
|
"web_visibility": "unlisted",
|
||||||
"sensitive": false,
|
"sensitive": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"status_content_type": "text/plain",
|
"status_content_type": "text/plain",
|
||||||
|
|
|
@ -658,6 +658,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
Language: "en",
|
Language: "en",
|
||||||
EnableRSS: util.Ptr(false),
|
EnableRSS: util.Ptr(false),
|
||||||
HideCollections: util.Ptr(false),
|
HideCollections: util.Ptr(false),
|
||||||
|
WebVisibility: gtsmodel.VisibilityPublic,
|
||||||
},
|
},
|
||||||
"admin_account": {
|
"admin_account": {
|
||||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
@ -668,6 +669,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
Language: "en",
|
Language: "en",
|
||||||
EnableRSS: util.Ptr(true),
|
EnableRSS: util.Ptr(true),
|
||||||
HideCollections: util.Ptr(false),
|
HideCollections: util.Ptr(false),
|
||||||
|
WebVisibility: gtsmodel.VisibilityPublic,
|
||||||
},
|
},
|
||||||
"local_account_1": {
|
"local_account_1": {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
|
@ -678,6 +680,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
Language: "en",
|
Language: "en",
|
||||||
EnableRSS: util.Ptr(true),
|
EnableRSS: util.Ptr(true),
|
||||||
HideCollections: util.Ptr(false),
|
HideCollections: util.Ptr(false),
|
||||||
|
WebVisibility: gtsmodel.VisibilityUnlocked,
|
||||||
},
|
},
|
||||||
"local_account_2": {
|
"local_account_2": {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
@ -688,6 +691,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
Language: "fr",
|
Language: "fr",
|
||||||
EnableRSS: util.Ptr(false),
|
EnableRSS: util.Ptr(false),
|
||||||
HideCollections: util.Ptr(true),
|
HideCollections: util.Ptr(true),
|
||||||
|
WebVisibility: gtsmodel.VisibilityPublic,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
discoverable: useBoolInput("discoverable", { source: profile}),
|
discoverable: useBoolInput("discoverable", { source: profile}),
|
||||||
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
||||||
hideCollections: useBoolInput("hide_collections", { source: profile }),
|
hideCollections: useBoolInput("hide_collections", { source: profile }),
|
||||||
|
webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p) => p.source?.web_visibility }),
|
||||||
fields: useFieldArrayInput("fields_attributes", {
|
fields: useFieldArrayInput("fields_attributes", {
|
||||||
defaultValue: profile?.source?.fields,
|
defaultValue: profile?.source?.fields,
|
||||||
length: instanceConfig.maxPinnedFields
|
length: instanceConfig.maxPinnedFields
|
||||||
|
@ -233,21 +234,32 @@ function UserProfileForm({ data: profile }) {
|
||||||
Learn more about these settings (opens in a new tab)
|
Learn more about these settings (opens in a new tab)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<Select
|
||||||
|
field={form.webVisibility}
|
||||||
|
label="Visibility level of posts to show on your profile, and in your RSS feed (if enabled)."
|
||||||
|
options={
|
||||||
|
<>
|
||||||
|
<option value="public">Show Public posts only (the GoToSocial default)</option>
|
||||||
|
<option value="unlisted">Show Public and Unlisted posts (the Mastodon default)</option>
|
||||||
|
<option value="none">Show no posts</option>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
field={form.locked}
|
field={form.locked}
|
||||||
label="Manually approve follow requests"
|
label="Manually approve follow requests."
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
field={form.discoverable}
|
field={form.discoverable}
|
||||||
label="Mark account as discoverable by search engines and directories"
|
label="Mark account as discoverable by search engines and directories."
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
field={form.enableRSS}
|
field={form.enableRSS}
|
||||||
label="Enable RSS feed of Public posts"
|
label="Enable RSS feed of posts."
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
field={form.hideCollections}
|
field={form.hideCollections}
|
||||||
label="Hide who you follow / are followed by"
|
label="Hide who you follow / are followed by."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form-section-docs">
|
<div className="form-section-docs">
|
||||||
|
|
Loading…
Reference in a new issue