mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-10 11:58:07 -06: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
|
|
@ -145,6 +145,15 @@ import (
|
|||
// description: Hide the account's following/followers collections.
|
||||
// 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]
|
||||
// in: formData
|
||||
// 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.CustomCSS == nil &&
|
||||
form.EnableRSS == nil &&
|
||||
form.HideCollections == nil) {
|
||||
form.HideCollections == nil &&
|
||||
form.WebVisibility == nil) {
|
||||
return nil, errors.New("empty form submitted")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,9 @@ type UpdateCredentialsRequest struct {
|
|||
EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
|
||||
// Hide this account's following/followers 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.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ type Source struct {
|
|||
// private = Followers-only post
|
||||
// direct = Direct post
|
||||
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.
|
||||
Sensitive bool `json:"sensitive"`
|
||||
// The default posting language for new statuses.
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ type StatusCreateRequest struct {
|
|||
type Visibility string
|
||||
|
||||
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 Visibility = "public"
|
||||
// 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.
|
||||
GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for returning statuses that
|
||||
// should be visible via the web view of an account. So, only public, federated statuses that aren't boosts
|
||||
// or replies.
|
||||
// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for
|
||||
// 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, 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(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)
|
||||
}
|
||||
|
||||
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
|
||||
if 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")).
|
||||
// Select only IDs from table
|
||||
Column("status.id").
|
||||
Where("? = ?", bun.Ident("status.account_id"), accountID).
|
||||
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("? 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.
|
||||
Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic).
|
||||
// Don't show local-only statuses on the web view.
|
||||
Where("? = ?", bun.Ident("status.federated"), true)
|
||||
q = q.Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic)
|
||||
|
||||
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
|
||||
if maxID == "" {
|
||||
|
|
@ -1145,10 +1178,30 @@ func (a *accountDB) UpdateAccountSettings(
|
|||
) error {
|
||||
return a.state.Caches.DB.AccountSettings.Store(settings, func() error {
|
||||
settings.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
|
||||
switch {
|
||||
|
||||
case len(columns) != 0:
|
||||
// If we're updating by column,
|
||||
// ensure "updated_at" is included.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
// By default we assume no auth.
|
||||
requesterID := noauth
|
||||
requesterID := NoAuth
|
||||
|
||||
if requester != nil {
|
||||
// Use provided account ID.
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import (
|
|||
"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.
|
||||
const noauth = "noauth"
|
||||
const NoAuth = "noauth"
|
||||
|
||||
// Filter packages up a bunch of logic for checking whether
|
||||
// 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
|
||||
|
||||
// By default we assume no auth.
|
||||
requesterID := noauth
|
||||
requesterID := NoAuth
|
||||
|
||||
if owner != nil {
|
||||
// Use provided account ID.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func (f *Filter) StatusPublicTimelineable(ctx context.Context, requester *gtsmod
|
|||
const vtype = cache.VisibilityTypePublic
|
||||
|
||||
// By default we assume no auth.
|
||||
requesterID := noauth
|
||||
requesterID := NoAuth
|
||||
|
||||
if requester != nil {
|
||||
// Use provided account ID.
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func (f *Filter) StatusVisible(
|
|||
const vtype = cache.VisibilityTypeStatus
|
||||
|
||||
// By default we assume no auth.
|
||||
requesterID := noauth
|
||||
requesterID := NoAuth
|
||||
|
||||
if requester != nil {
|
||||
// Use provided account ID.
|
||||
|
|
@ -113,9 +113,9 @@ func (f *Filter) isStatusVisible(
|
|||
}
|
||||
|
||||
if requester == nil {
|
||||
// The request is unauthed. Only federated, Public statuses are visible without auth.
|
||||
visibleUnauthed := !status.IsLocalOnly() && status.Visibility == gtsmodel.VisibilityPublic
|
||||
return visibleUnauthed, nil
|
||||
// Use a different visibility
|
||||
// heuristic for unauthed requests.
|
||||
return f.isStatusVisibleUnauthed(ctx, status)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -245,6 +245,62 @@ func (f *Filter) isPendingStatusVisible(
|
|||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AccountSettings models settings / preferences for a local, non-instance account.
|
||||
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.
|
||||
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: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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -238,6 +238,9 @@ type StatusToEmoji struct {
|
|||
type Visibility string
|
||||
|
||||
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 Visibility = "public"
|
||||
// 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
|
||||
|
||||
// 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) {
|
||||
err = fmt.Errorf("db error getting account web statuses: %w", err)
|
||||
return "", gtserror.NewErrorInternalError(err)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ func (p *Processor) WebStatusesGet(
|
|||
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) {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -206,9 +206,15 @@ func (p *Processor) WebStatusesGetPinned(
|
|||
|
||||
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
if status.Visibility != gtsmodel.VisibilityPublic {
|
||||
// Skip non-public
|
||||
// pinned status.
|
||||
// Ensure visible via the web.
|
||||
visible, err := p.visFilter.StatusVisible(ctx, nil, status)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error checking status visibility: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !visible {
|
||||
// Don't serve.
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
account.Discoverable = form.Discoverable
|
||||
acctColumns = append(acctColumns, "discoverable")
|
||||
}
|
||||
|
||||
if form.Bot != nil {
|
||||
account.Bot = form.Bot
|
||||
acctColumns = append(acctColumns, "bot")
|
||||
}
|
||||
|
||||
// Via the process of updating the account,
|
||||
// it is possible that the emojis used by
|
||||
// that account in note/display name/fields
|
||||
// may change; we need to keep track of this.
|
||||
var emojisChanged bool
|
||||
if form.Locked != nil {
|
||||
account.Locked = form.Locked
|
||||
acctColumns = append(acctColumns, "locked")
|
||||
}
|
||||
|
||||
if form.DisplayName != nil {
|
||||
// Display name text
|
||||
// is changing.
|
||||
textChanged = true
|
||||
|
||||
displayName := *form.DisplayName
|
||||
if err := validate.DisplayName(displayName); err != nil {
|
||||
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).
|
||||
account.DisplayName = text.SanitizeToPlaintext(displayName)
|
||||
|
||||
// If display name has changed, account emojis may have also changed.
|
||||
emojisChanged = true
|
||||
acctColumns = append(acctColumns, "display_name")
|
||||
}
|
||||
|
||||
if form.Note != nil {
|
||||
// Note text is changing.
|
||||
textChanged = true
|
||||
|
||||
note := *form.Note
|
||||
if err := validate.Note(note); err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
// Store raw version of the note for now,
|
||||
// we'll process the proper version later.
|
||||
// Store raw version of note
|
||||
// for now, we'll process
|
||||
// the proper version later.
|
||||
account.NoteRaw = note
|
||||
|
||||
// If note has changed, account emojis may have also changed.
|
||||
emojisChanged = true
|
||||
acctColumns = append(acctColumns, []string{
|
||||
"note",
|
||||
"note_raw",
|
||||
}...)
|
||||
}
|
||||
|
||||
if form.FieldsAttributes != nil {
|
||||
var (
|
||||
fieldsAttributes = *form.FieldsAttributes
|
||||
fieldsLen = len(fieldsAttributes)
|
||||
fieldsRaw = make([]*gtsmodel.Field, 0, fieldsLen)
|
||||
)
|
||||
// Field text is changing.
|
||||
textChanged = true
|
||||
|
||||
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)
|
||||
if err := p.updateFields(
|
||||
account,
|
||||
*form.FieldsAttributes,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check length of parsed raw fields.
|
||||
if err := validate.ProfileFields(fieldsRaw); err != nil {
|
||||
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
|
||||
acctColumns = append(acctColumns, []string{
|
||||
"fields",
|
||||
"fields_raw",
|
||||
}...)
|
||||
}
|
||||
|
||||
if emojisChanged {
|
||||
// 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 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 textChanged {
|
||||
// Process display name, note, fields,
|
||||
// and any concomitant emoji changes.
|
||||
p.processAccountText(ctx, account)
|
||||
acctColumns = append(acctColumns, "emojis")
|
||||
}
|
||||
|
||||
if form.AvatarDescription != nil {
|
||||
desc := text.SanitizeToPlaintext(*form.AvatarDescription)
|
||||
form.AvatarDescription = util.Ptr(desc)
|
||||
form.AvatarDescription = &desc
|
||||
}
|
||||
|
||||
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.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 {
|
||||
// Update just existing description if possible.
|
||||
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.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 {
|
||||
// Update just existing description if possible.
|
||||
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.Locked = form.Locked
|
||||
}
|
||||
// Account settings flags.
|
||||
|
||||
if form.Source != nil {
|
||||
if form.Source.Language != nil {
|
||||
language, err := validate.Language(*form.Source.Language)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err)
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
account.Settings.Language = language
|
||||
settingsColumns = append(settingsColumns, "language")
|
||||
}
|
||||
|
||||
if form.Source.Sensitive != nil {
|
||||
account.Settings.Sensitive = form.Source.Sensitive
|
||||
settingsColumns = append(settingsColumns, "sensitive")
|
||||
}
|
||||
|
||||
if form.Source.Privacy != 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 {
|
||||
|
|
@ -295,6 +238,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
settingsColumns = append(settingsColumns, "theme")
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS)
|
||||
settingsColumns = append(settingsColumns, "custom_css")
|
||||
}
|
||||
|
||||
if form.EnableRSS != nil {
|
||||
account.Settings.EnableRSS = form.EnableRSS
|
||||
settingsColumns = append(settingsColumns, "enable_rss")
|
||||
}
|
||||
|
||||
if form.HideCollections != nil {
|
||||
account.Settings.HideCollections = form.HideCollections
|
||||
settingsColumns = append(settingsColumns, "hide_collections")
|
||||
}
|
||||
|
||||
if err := p.state.DB.UpdateAccount(ctx, account); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
|
||||
if form.WebVisibility != nil {
|
||||
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 {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account settings %s: %s", account.ID, err))
|
||||
// We've parsed + set everything, do
|
||||
// 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{
|
||||
APObjectType: ap.ActorPerson,
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// part of an account update form, parsing and checking the
|
||||
// media, and doing the necessary updates in the database
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ func APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility {
|
|||
return gtsmodel.VisibilityMutualsOnly
|
||||
case apimodel.VisibilityDirect:
|
||||
return gtsmodel.VisibilityDirect
|
||||
case apimodel.VisibilityNone:
|
||||
return gtsmodel.VisibilityNone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,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),
|
||||
Sensitive: *a.Settings.Sensitive,
|
||||
Language: a.Settings.Language,
|
||||
StatusContentType: statusContentType,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
|||
"fields": [],
|
||||
"source": {
|
||||
"privacy": "public",
|
||||
"web_visibility": "unlisted",
|
||||
"sensitive": false,
|
||||
"language": "en",
|
||||
"status_content_type": "text/plain",
|
||||
|
|
@ -304,6 +305,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
|||
"fields": [],
|
||||
"source": {
|
||||
"privacy": "public",
|
||||
"web_visibility": "unlisted",
|
||||
"sensitive": false,
|
||||
"language": "en",
|
||||
"status_content_type": "text/plain",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue