From 0b0f3d9e9adbdd9e93c01629c21f274e094ebe54 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 2 Apr 2021 19:20:41 +0200 Subject: [PATCH] additional work on statuses --- internal/apimodule/status/statuscreate.go | 68 ++++++++++++++++++----- internal/config/config.go | 24 ++++---- internal/config/statuses.go | 8 +-- internal/db/model/mention.go | 39 +++++++++++++ internal/db/model/status.go | 49 +++++++++++----- internal/util/parse.go | 18 ++++++ pkg/mastotypes/status.go | 21 +++++-- 7 files changed, 178 insertions(+), 49 deletions(-) create mode 100644 internal/db/model/mention.go diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go index 587f4ed..ad6c619 100644 --- a/internal/apimodule/status/statuscreate.go +++ b/internal/apimodule/status/statuscreate.go @@ -27,6 +27,7 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" @@ -57,7 +58,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { } l.Tracef("validating form %+v", form) - if err := validateCreateStatus(form, m.config.StatusesConfig, m.db); err != nil { + if err := validateCreateStatus(form, m.config.StatusesConfig, authed.Account.ID, m.db); err != nil { l.Debugf("error validating form: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -71,16 +72,15 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) return } + + // newStatus := &model.Status{ + + // } + } -func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.StatusesConfig, db db.DB) error { - - if form.Language != "" { - if err := util.ValidateLanguage(form.Language); err != nil { - return err - } - } - +func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.StatusesConfig, accountID string, db db.DB) error { + // validate that, structurally, we have a valid status/post if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { return errors.New("no status, media, or poll provided") } @@ -89,6 +89,31 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S return errors.New("can't post media + poll in same status") } + // validate status + if form.Status != "" { + if len(form.Status) > config.MaxChars { + return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) + } + } + + // validate media attachments + if len(form.MediaIDs) > config.MaxMediaFiles { + return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) + } + + for _, m := range form.MediaIDs { + // check these attachments exist + a := &model.MediaAttachment{} + if err := db.GetByID(m, a); err != nil { + return fmt.Errorf("invalid media type or media not found for media id %s: %s", m, err) + } + // check they belong to the requesting account id + if a.AccountID != accountID { + return fmt.Errorf("media attachment %s does not belong to account id %s", m, accountID) + } + } + + // validate poll if form.Poll != nil { if form.Poll.Options == nil { return errors.New("poll with no options") @@ -103,13 +128,28 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S } } - if len(form.MediaIDs) > config.MaxMediaFiles { - return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) + // validate reply-to status exists and is reply-able + if form.InReplyToID != "" { + s := &model.Status{} + if err := db.GetByID(form.InReplyToID, s); err != nil { + return fmt.Errorf("status id %s cannot be retrieved from the db: %s", form.InReplyToID, err) + } + if !*s.VisibilityAdvanced.Replyable { + return fmt.Errorf("status with id %s is not replyable", form.InReplyToID) + } } - if form.Status != "" { - if len(form.Status) > config.MaxChars { - return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) + // validate spoiler text/cw + if form.SpoilerText != "" { + if len(form.SpoilerText) > config.CWMaxChars { + return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars) + } + } + + // validate post language + if form.Language != "" { + if err := util.ValidateLanguage(form.Language); err != nil { + return err } } diff --git a/internal/config/config.go b/internal/config/config.go index 59023d3..4cb2b90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -233,11 +233,11 @@ type Flags struct { StorageServeHost string StorageServeBasePath string - StatusesMaxChars string - StatusesCWMaxChars string - StatusesPollMaxOptions string + StatusesMaxChars string + StatusesCWMaxChars string + StatusesPollMaxOptions string StatusesPollOptionMaxChars string - StatusesMaxMediaFiles string + StatusesMaxMediaFiles string } // GetFlagNames returns a struct containing the names of the various flags used for @@ -271,11 +271,11 @@ func GetFlagNames() Flags { StorageServeHost: "storage-serve-host", StorageServeBasePath: "storage-serve-base-path", - StatusesMaxChars: "statuses-max-chars", - StatusesCWMaxChars: "statuses-cw-max-chars", - StatusesPollMaxOptions: "statuses-poll-max-options", + StatusesMaxChars: "statuses-max-chars", + StatusesCWMaxChars: "statuses-cw-max-chars", + StatusesPollMaxOptions: "statuses-poll-max-options", StatusesPollOptionMaxChars: "statuses-poll-option-max-chars", - StatusesMaxMediaFiles: "statuses-max-media-files", + StatusesMaxMediaFiles: "statuses-max-media-files", } } @@ -310,10 +310,10 @@ func GetEnvNames() Flags { StorageServeHost: "GTS_STORAGE_SERVE_HOST", StorageServeBasePath: "GTS_STORAGE_SERVE_BASE_PATH", - StatusesMaxChars: "GTS_STATUSES_MAX_CHARS", - StatusesCWMaxChars: "GTS_STATUSES_CW_MAX_CHARS", - StatusesPollMaxOptions: "GTS_STATUSES_POLL_MAX_OPTIONS", + StatusesMaxChars: "GTS_STATUSES_MAX_CHARS", + StatusesCWMaxChars: "GTS_STATUSES_CW_MAX_CHARS", + StatusesPollMaxOptions: "GTS_STATUSES_POLL_MAX_OPTIONS", StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS", - StatusesMaxMediaFiles: "GTS_STATUSES_MAX_MEDIA_FILES", + StatusesMaxMediaFiles: "GTS_STATUSES_MAX_MEDIA_FILES", } } diff --git a/internal/config/statuses.go b/internal/config/statuses.go index bdc50d5..fbb5225 100644 --- a/internal/config/statuses.go +++ b/internal/config/statuses.go @@ -21,13 +21,13 @@ package config // StatusesConfig pertains to posting/deleting/interacting with statuses type StatusesConfig struct { // Maximum amount of characters allowed in a status, excluding CW - MaxChars int `yaml:"max_chars"` + MaxChars int `yaml:"max_chars"` // Maximum amount of characters allowed in a content-warning/spoiler field - CWMaxChars int `yaml:"cw_max_chars"` + CWMaxChars int `yaml:"cw_max_chars"` // Maximum number of options allowed in a poll - PollMaxOptions int `yaml:"poll_max_options"` + PollMaxOptions int `yaml:"poll_max_options"` // Maximum characters allowed per poll option PollOptionMaxChars int `yaml:"poll_option_max_chars"` // Maximum amount of media files allowed to be attached to one status - MaxMediaFiles int `yaml:"max_media_files"` + MaxMediaFiles int `yaml:"max_media_files"` } diff --git a/internal/db/model/mention.go b/internal/db/model/mention.go new file mode 100644 index 0000000..74dd001 --- /dev/null +++ b/internal/db/model/mention.go @@ -0,0 +1,39 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + +package model + +import "time" + +// Mention refers to the 'tagging' or 'mention' of a user within a status. +type Mention struct { + // ID of this mention in the database + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + // ID of the status this mention originates from + StatusID string + // When was this mention created? + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // When was this mention last updated? + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // Who created this mention? + OriginAccountID string + // Who does this mention target? + TargetAccountID string + // Prevent this mention from generating a notification? + Silent bool +} diff --git a/internal/db/model/status.go b/internal/db/model/status.go index d51ae28..31f5623 100644 --- a/internal/db/model/status.go +++ b/internal/db/model/status.go @@ -45,22 +45,45 @@ type Status struct { // cw string for this status ContentWarning string // visibility entry for this status - Visibility *Visibility + Visibility Visibility + // advanced visibility for this status + VisibilityAdvanced VisibilityAdvanced // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types // Will probably almost always be a note. ActivityStreamsType string } -// Visibility represents the visibility granularity of a status. It is a combination of flags. -type Visibility struct { - // Is this status viewable as a direct message? - Direct bool - // Is this status viewable to followers? - Followers bool - // Is this status viewable on the local timeline? - Local bool - // Is this status boostable but not shown on public timelines? - Unlisted bool - // Is this status shown on public and federated timelines? - Public bool +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( + // This status will be visible to everyone on all timelines. + VisibilityPublic Visibility = "public" + // This status will be visible to everyone, but will only show on home timeline to followers, and in lists. + VisibilityUnlocked Visibility = "unlocked" + // This status is viewable to followers only. + VisibilityFollowersOnly Visibility = "followers_only" + // This status is visible to mutual followers only. + VisibilityMutualsOnly Visibility = "mutuals_only" + // This status is visible only to mentioned recipients + VisibilityDirect Visibility = "direct" +) + +type VisibilityAdvanced struct { + /* + ADVANCED SETTINGS -- These should all default to TRUE. + + If PUBLIC is selected, they will all be overwritten to TRUE regardless of what is selected. + If UNLOCKED is selected, any of them can be turned on or off in any combination. + If FOLLOWERS-ONLY or MUTUALS-ONLY are selected, boostable will always be FALSE. The others can be turned on or off as desired. + If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE. + */ + // This status will be federated beyond the local timeline(s) + Federated *bool `pg:"default:true"` + // This status can be boosted/reblogged + Boostable *bool `pg:"default:true"` + // This status can be replied to + Replyable *bool `pg:"default:true"` + // This status can be liked/faved + Likeable *bool `pg:"default:true"` } diff --git a/internal/util/parse.go b/internal/util/parse.go index 375ab97..fec88e7 100644 --- a/internal/util/parse.go +++ b/internal/util/parse.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + 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 . +*/ + package util import "fmt" diff --git a/pkg/mastotypes/status.go b/pkg/mastotypes/status.go index d88e7df..5f2c73f 100644 --- a/pkg/mastotypes/status.go +++ b/pkg/mastotypes/status.go @@ -33,11 +33,7 @@ type Status struct { // Subject or summary line, below which status content is collapsed until expanded. SpoilerText string `json:"spoiler_text"` // Visibility of this status. - // public = Visible to everyone, shown in public timelines. - // unlisted = Visible to public, but not included in public timelines. - // private = Visible to followers only, and to any mentioned users. - // direct = Visible only to mentioned users. - Visibility string `json:"visibility"` + Visibility Visibility `json:"visibility"` // Primary language of this status. (ISO 639 Part 1 two-letter language code) Language string `json:"language"` // URI of the status used for federation. @@ -102,9 +98,22 @@ type StatusCreateRequest struct { // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. SpoilerText string `form:"spoiler_text"` // Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. - Visibility string `form:"visibility"` + Visibility Visibility `form:"visibility"` // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. ScheduledAt string `form:"scheduled_at"` // ISO 639 language code for this status. Language string `form:"language"` } + +type Visibility string + +const ( + // visible to everyone + VisibilityPublic Visibility = "public" + // visible to everyone but only on home timelines or in lists + VisibilityUnlisted Visibility = "unlisted" + // visible to followers only + VisibilityPrivate Visibility = "private" + // visible only to tagged recipients + VisibilityDirect Visibility = "direct" +)