diff options
author | 2024-01-05 13:39:31 +0100 | |
---|---|---|
committer | 2024-01-05 13:39:31 +0100 | |
commit | d5e3996a18ee37fc4bdf5718632d3d19ac7a8c1b (patch) | |
tree | 7b8df224893611c77cadc847c2fd8d7ec239b636 /internal/processing | |
parent | [bugfix] fix check for closed poll to account for non-zero closed time but in... (diff) | |
download | gotosocial-d5e3996a18ee37fc4bdf5718632d3d19ac7a8c1b.tar.xz |
[feature] Parse instance descriptors as markdown, show T&C on /about (#2481)
* [feature] Parse instance descriptors as markdown, show T&C on /about
* lint
* remove unnecessary nullzero tags
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/instance.go | 228 | ||||
-rw-r--r-- | internal/processing/processor.go | 17 |
2 files changed, 155 insertions, 90 deletions
diff --git a/internal/processing/instance.go b/internal/processing/instance.go index caf2b9fc1..a93936425 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -33,15 +33,6 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/validate" ) -func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) { - instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) - if err != nil { - return nil, err - } - - return instance, nil -} - func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { i, err := p.getThisInstance(ctx) if err != nil { @@ -146,67 +137,55 @@ func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRu } func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) { - // fetch the instance entry from the db for processing - host := config.GetHost() - - instance, err := p.state.DB.GetInstance(ctx, host) + // Fetch this instance from the db for processing. + instance, err := p.getThisInstance(ctx) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", host, err)) + err = fmt.Errorf("db error fetching instance: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // fetch the instance account from the db for processing - ia, err := p.state.DB.GetInstanceAccount(ctx, "") + // Fetch this instance account from the db for processing. + instanceAcc, err := p.state.DB.GetInstanceAccount(ctx, "") if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", host, err)) + err = fmt.Errorf("db error fetching instance account: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - updatingColumns := []string{} + // Columns to update + // in the database. + var columns []string - // validate & update site title if it's set on the form + // Validate & update site + // title if set on the form. if form.Title != nil { - if err := validate.SiteTitle(*form.Title); err != nil { - return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("site title invalid: %s", err)) + title := *form.Title + if err := validate.SiteTitle(title); err != nil { + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - updatingColumns = append(updatingColumns, "title") - instance.Title = text.SanitizeToPlaintext(*form.Title) // don't allow html in site title + + // Don't allow html in site title. + instance.Title = text.SanitizeToPlaintext(title) + columns = append(columns, "title") } - // validate & update site contact account if it's set on the form + // Validate & update site contact + // account if set on the form. + // + // Empty username unsets contact. if form.ContactUsername != nil { - // make sure the account with the given username exists in the db - contactAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, *form.ContactUsername, "") - if err != nil { - return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.ContactUsername)) - } - // make sure it has a user associated with it - contactUser, err := p.state.DB.GetUserByAccountID(ctx, contactAccount.ID) + contactAccountID, err := p.contactAccountIDForUsername(ctx, *form.ContactUsername) if err != nil { - return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("user for account with username %s not retrievable", *form.ContactUsername)) - } - // suspended accounts cannot be contact accounts - if !contactAccount.SuspendedAt.IsZero() { - err := fmt.Errorf("selected contact account %s is suspended", contactAccount.Username) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - // unconfirmed or unapproved users cannot be contacts - if contactUser.ConfirmedAt.IsZero() { - err := fmt.Errorf("user of selected contact account %s is not confirmed", contactAccount.Username) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - if !*contactUser.Approved { - err := fmt.Errorf("user of selected contact account %s is not approved", contactAccount.Username) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - // contact account user must be admin or moderator otherwise what's the point of contacting them - if !*contactUser.Admin && !*contactUser.Moderator { - err := fmt.Errorf("user of selected contact account %s is neither admin nor moderator", contactAccount.Username) return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - updatingColumns = append(updatingColumns, "contact_account_id") - instance.ContactAccountID = contactAccount.ID + + columns = append(columns, "contact_account_id") + instance.ContactAccountID = contactAccountID } - // validate & update site contact email if it's set on the form + // Validate & update contact + // email if set on the form. + // + // Empty email unsets contact. if form.ContactEmail != nil { contactEmail := *form.ContactEmail if contactEmail != "" { @@ -214,87 +193,162 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe return nil, gtserror.NewErrorBadRequest(err, err.Error()) } } - updatingColumns = append(updatingColumns, "contact_email") + + columns = append(columns, "contact_email") instance.ContactEmail = contactEmail } - // validate & update site short description if it's set on the form + // Validate & update site short + // description if set on the form. if form.ShortDescription != nil { - if err := validate.SiteShortDescription(*form.ShortDescription); err != nil { + shortDescription := *form.ShortDescription + if err := validate.SiteShortDescription(shortDescription); err != nil { return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - updatingColumns = append(updatingColumns, "short_description") - instance.ShortDescription = text.SanitizeToHTML(*form.ShortDescription) // html is OK in site description, but we should sanitize it + + // Parse description as Markdown, keep + // the raw version for later editing. + instance.ShortDescriptionText = shortDescription + instance.ShortDescription = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, instanceAcc.ID, "", shortDescription).HTML + columns = append(columns, []string{"short_description", "short_description_text"}...) } // validate & update site description if it's set on the form if form.Description != nil { - if err := validate.SiteDescription(*form.Description); err != nil { + description := *form.Description + if err := validate.SiteDescription(description); err != nil { return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - updatingColumns = append(updatingColumns, "description") - instance.Description = text.SanitizeToHTML(*form.Description) // html is OK in site description, but we should sanitize it + + // Parse description as Markdown, keep + // the raw version for later editing. + instance.DescriptionText = description + instance.Description = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, instanceAcc.ID, "", description).HTML + columns = append(columns, []string{"description", "description_text"}...) } - // validate & update site terms if it's set on the form + // Validate & update site + // terms if set on the form. if form.Terms != nil { - if err := validate.SiteTerms(*form.Terms); err != nil { + terms := *form.Terms + if err := validate.SiteTerms(terms); err != nil { return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - updatingColumns = append(updatingColumns, "terms") - instance.Terms = text.SanitizeToHTML(*form.Terms) // html is OK in site terms, but we should sanitize it + + // Parse terms as Markdown, keep + // the raw version for later editing. + instance.TermsText = terms + instance.Terms = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, "", "", terms).HTML + columns = append(columns, []string{"terms", "terms_text"}...) } var updateInstanceAccount bool if form.Avatar != nil && form.Avatar.Size != 0 { - // process instance avatar image + description - avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID) + // Process instance avatar image + description. + avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, instanceAcc.ID) if err != nil { return nil, gtserror.NewErrorBadRequest(err, "error processing avatar") } - ia.AvatarMediaAttachmentID = avatarInfo.ID - ia.AvatarMediaAttachment = avatarInfo + instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID + instanceAcc.AvatarMediaAttachment = avatarInfo updateInstanceAccount = true - } else if form.AvatarDescription != nil && ia.AvatarMediaAttachment != nil { - // process just the description for the existing avatar - ia.AvatarMediaAttachment.Description = *form.AvatarDescription - if err := p.state.DB.UpdateAttachment(ctx, ia.AvatarMediaAttachment, "description"); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance avatar description: %s", err)) + } else if form.AvatarDescription != nil && instanceAcc.AvatarMediaAttachment != nil { + // Process just the description for the existing avatar. + instanceAcc.AvatarMediaAttachment.Description = *form.AvatarDescription + if err := p.state.DB.UpdateAttachment(ctx, instanceAcc.AvatarMediaAttachment, "description"); err != nil { + err = fmt.Errorf("db error updating instance avatar description: %w", err) + return nil, gtserror.NewErrorInternalError(err) } } if form.Header != nil && form.Header.Size != 0 { // process instance header image - headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, ia.ID) + headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, instanceAcc.ID) if err != nil { return nil, gtserror.NewErrorBadRequest(err, "error processing header") } - ia.HeaderMediaAttachmentID = headerInfo.ID - ia.HeaderMediaAttachment = headerInfo + instanceAcc.HeaderMediaAttachmentID = headerInfo.ID + instanceAcc.HeaderMediaAttachment = headerInfo updateInstanceAccount = true } if updateInstanceAccount { - // if either avatar or header is updated, we need - // to update the instance account that stores them - if err := p.state.DB.UpdateAccount(ctx, ia); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance account: %s", err)) + // If either avatar or header is updated, we need + // to update the instance account that stores them. + if err := p.state.DB.UpdateAccount(ctx, instanceAcc); err != nil { + err = fmt.Errorf("db error updating instance account: %w", err) + return nil, gtserror.NewErrorInternalError(err, err.Error()) } } - if len(updatingColumns) != 0 { - if err := p.state.DB.UpdateInstance(ctx, instance, updatingColumns...); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance %s: %s", host, err)) + if len(columns) != 0 { + if err := p.state.DB.UpdateInstance(ctx, instance, columns...); err != nil { + err = fmt.Errorf("db error updating instance: %w", err) + return nil, gtserror.NewErrorInternalError(err, err.Error()) } } - ai, err := p.converter.InstanceToAPIV1Instance(ctx, instance) + return p.InstanceGetV1(ctx) +} + +func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) { + instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) + return nil, err } - return ai, nil + return instance, nil +} + +func (p *Processor) contactAccountIDForUsername(ctx context.Context, username string) (string, error) { + if username == "" { + // Easy: unset + // contact account. + return "", nil + } + + // Make sure local account with the given username exists in the db. + contactAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") + if err != nil { + err = fmt.Errorf("db error getting selected contact account with username %s: %w", username, err) + return "", err + } + + // Make sure account corresponds to a user. + contactUser, err := p.state.DB.GetUserByAccountID(ctx, contactAccount.ID) + if err != nil { + err = fmt.Errorf("db error getting user for selected contact account %s: %w", username, err) + return "", err + } + + // Ensure account/user is: + // + // - confirmed and approved + // - not suspended + // - an admin or a moderator + if contactUser.ConfirmedAt.IsZero() { + err := fmt.Errorf("user of selected contact account %s is not confirmed", contactAccount.Username) + return "", err + } + + if !*contactUser.Approved { + err := fmt.Errorf("user of selected contact account %s is not approved", contactAccount.Username) + return "", err + } + + if !contactAccount.SuspendedAt.IsZero() { + err := fmt.Errorf("selected contact account %s is suspended", contactAccount.Username) + return "", err + } + + if !*contactUser.Admin && !*contactUser.Moderator { + err := fmt.Errorf("user of selected contact account %s is neither admin nor moderator", contactAccount.Username) + return "", err + } + + // All good! + return contactAccount.ID, nil } func obfuscate(domain string) string { diff --git a/internal/processing/processor.go b/internal/processing/processor.go index ac930aeb2..6ac7a6bf0 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -21,6 +21,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/cleaner" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" mm "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/processing/account" @@ -39,6 +40,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/processing/user" "github.com/superseriousbusiness/gotosocial/internal/processing/workers" "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" ) @@ -56,6 +58,13 @@ type Processor struct { state *state.State /* + Required for instance description / terms updating. + */ + + formatter *text.Formatter + parseMentionFunc gtsmodel.ParseMentionFunc + + /* SUB-PROCESSORS */ @@ -147,9 +156,11 @@ func NewProcessor( ) processor := &Processor{ - converter: converter, - oauthServer: oauthServer, - state: state, + converter: converter, + oauthServer: oauthServer, + state: state, + formatter: text.NewFormatter(state.DB), + parseMentionFunc: parseMentionFunc, } // Instantiate sub processors. |