start on status creation
This commit is contained in:
parent
71a49e2b43
commit
e58229175d
@ -175,6 +175,38 @@ func main() {
|
||||
Value: "/fileserver/media",
|
||||
EnvVars: []string{envNames.StorageServeBasePath},
|
||||
},
|
||||
|
||||
// STATUSES FLAGS
|
||||
&cli.IntFlag{
|
||||
Name: flagNames.StatusesMaxChars,
|
||||
Usage: "Max permitted characters for posted statuses",
|
||||
Value: 5000,
|
||||
EnvVars: []string{envNames.StatusesMaxChars},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flagNames.StatusesCWMaxChars,
|
||||
Usage: "Max permitted characters for content/spoiler warnings on statuses",
|
||||
Value: 100,
|
||||
EnvVars: []string{envNames.StatusesCWMaxChars},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flagNames.StatusesPollMaxOptions,
|
||||
Usage: "Max amount of options permitted on a poll",
|
||||
Value: 6,
|
||||
EnvVars: []string{envNames.StatusesPollMaxOptions},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flagNames.StatusesPollOptionMaxChars,
|
||||
Usage: "Max amount of characters for a poll option",
|
||||
Value: 50,
|
||||
EnvVars: []string{envNames.StatusesPollOptionMaxChars},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flagNames.StatusesMaxMediaFiles,
|
||||
Usage: "Maximum number of media files/attachments per status",
|
||||
Value: 6,
|
||||
EnvVars: []string{envNames.StatusesMaxMediaFiles},
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
|
95
internal/apimodule/status/status.go
Normal file
95
internal/apimodule/status/status.go
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||
)
|
||||
|
||||
const (
|
||||
idKey = "id"
|
||||
basePath = "/api/v1/statuses"
|
||||
basePathWithID = basePath + "/:" + idKey
|
||||
contextPath = basePath + "/context"
|
||||
rebloggedPath = basePath + "/reblogged_by"
|
||||
favouritedPath = basePath + "/favourited_by"
|
||||
favouritePath = basePath + "/favourite"
|
||||
reblogPath = basePath + "/reblog"
|
||||
unreblogPath = basePath + "/unreblog"
|
||||
bookmarkPath = basePath + "/bookmark"
|
||||
unbookmarkPath = basePath + "/unbookmark"
|
||||
mutePath = basePath + "/mute"
|
||||
unmutePath = basePath + "/unmute"
|
||||
pinPath = basePath + "/pin"
|
||||
unpinPath = basePath + "/unpin"
|
||||
)
|
||||
|
||||
type statusModule struct {
|
||||
config *config.Config
|
||||
db db.DB
|
||||
oauthServer oauth.Server
|
||||
mediaHandler media.MediaHandler
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// New returns a new account module
|
||||
func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, log *logrus.Logger) apimodule.ClientAPIModule {
|
||||
return &statusModule{
|
||||
config: config,
|
||||
db: db,
|
||||
mediaHandler: mediaHandler,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Route attaches all routes from this module to the given router
|
||||
func (m *statusModule) Route(r router.Router) error {
|
||||
// r.AttachHandler(http.MethodPost, basePath, m.accountCreatePOSTHandler)
|
||||
// r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *statusModule) CreateTables(db db.DB) error {
|
||||
models := []interface{}{
|
||||
&model.User{},
|
||||
&model.Account{},
|
||||
&model.Follow{},
|
||||
&model.FollowRequest{},
|
||||
&model.Status{},
|
||||
&model.Application{},
|
||||
&model.EmailDomainBlock{},
|
||||
&model.MediaAttachment{},
|
||||
}
|
||||
|
||||
for _, m := range models {
|
||||
if err := db.CreateTable(m); err != nil {
|
||||
return fmt.Errorf("error creating table: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
117
internal/apimodule/status/statuscreate.go
Normal file
117
internal/apimodule/status/statuscreate.go
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/pkg/mastotypes"
|
||||
)
|
||||
|
||||
func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||
l := m.log.WithField("func", "statusCreatePOSTHandler")
|
||||
authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything*
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// check this user/account is permitted to post new statuses
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
return
|
||||
}
|
||||
|
||||
l.Trace("parsing request form")
|
||||
form := &mastotypes.StatusCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil || form == nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||
return
|
||||
}
|
||||
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateStatus(form, m.config.StatusesConfig, m.db); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
l.Tracef("attempting to parse client ip address %s", clientIP)
|
||||
signUpIP := net.ParseIP(clientIP)
|
||||
if signUpIP == nil {
|
||||
l.Debugf("error validating client ip address %s", clientIP)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
|
||||
return errors.New("no status, media, or poll provided")
|
||||
}
|
||||
|
||||
if form.MediaIDs != nil && form.Poll != nil {
|
||||
return errors.New("can't post media + poll in same status")
|
||||
}
|
||||
|
||||
if form.Poll != nil {
|
||||
if form.Poll.Options == nil {
|
||||
return errors.New("poll with no options")
|
||||
}
|
||||
if len(form.Poll.Options) > config.PollMaxOptions {
|
||||
return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), config.PollMaxOptions)
|
||||
}
|
||||
for _, p := range form.Poll.Options {
|
||||
if len(p) > config.PollOptionMaxChars {
|
||||
return fmt.Errorf("poll option too long, %d characters provided but limit is %d", len(p), config.PollOptionMaxChars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -36,6 +36,7 @@ type Config struct {
|
||||
AccountsConfig *AccountsConfig `yaml:"accounts"`
|
||||
MediaConfig *MediaConfig `yaml:"media"`
|
||||
StorageConfig *StorageConfig `yaml:"storage"`
|
||||
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
||||
}
|
||||
|
||||
// FromFile returns a new config from a file, or an error if something goes amiss.
|
||||
@ -58,6 +59,7 @@ func Empty() *Config {
|
||||
AccountsConfig: &AccountsConfig{},
|
||||
MediaConfig: &MediaConfig{},
|
||||
StorageConfig: &StorageConfig{},
|
||||
StatusesConfig: &StatusesConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +175,23 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
|
||||
if c.StorageConfig.ServeBasePath == "" || f.IsSet(fn.StorageServeBasePath) {
|
||||
c.StorageConfig.ServeBasePath = f.String(fn.StorageServeBasePath)
|
||||
}
|
||||
|
||||
// statuses flags
|
||||
if c.StatusesConfig.MaxChars == 0 || f.IsSet(fn.StatusesMaxChars) {
|
||||
c.StatusesConfig.MaxChars = f.Int(fn.StatusesMaxChars)
|
||||
}
|
||||
if c.StatusesConfig.CWMaxChars == 0 || f.IsSet(fn.StatusesCWMaxChars) {
|
||||
c.StatusesConfig.CWMaxChars = f.Int(fn.StatusesCWMaxChars)
|
||||
}
|
||||
if c.StatusesConfig.PollMaxOptions == 0 || f.IsSet(fn.StatusesPollMaxOptions) {
|
||||
c.StatusesConfig.PollMaxOptions = f.Int(fn.StatusesPollMaxOptions)
|
||||
}
|
||||
if c.StatusesConfig.PollOptionMaxChars == 0 || f.IsSet(fn.StatusesPollOptionMaxChars) {
|
||||
c.StatusesConfig.PollOptionMaxChars = f.Int(fn.StatusesPollOptionMaxChars)
|
||||
}
|
||||
if c.StatusesConfig.MaxMediaFiles == 0 || f.IsSet(fn.StatusesMaxMediaFiles) {
|
||||
c.StatusesConfig.MaxMediaFiles = f.Int(fn.StatusesMaxMediaFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// KeyedFlags is a wrapper for any type that can store keyed flags and give them back.
|
||||
@ -213,6 +232,12 @@ type Flags struct {
|
||||
StorageServeProtocol string
|
||||
StorageServeHost string
|
||||
StorageServeBasePath string
|
||||
|
||||
StatusesMaxChars string
|
||||
StatusesCWMaxChars string
|
||||
StatusesPollMaxOptions string
|
||||
StatusesPollOptionMaxChars string
|
||||
StatusesMaxMediaFiles string
|
||||
}
|
||||
|
||||
// GetFlagNames returns a struct containing the names of the various flags used for
|
||||
@ -245,6 +270,12 @@ func GetFlagNames() Flags {
|
||||
StorageServeProtocol: "storage-serve-protocol",
|
||||
StorageServeHost: "storage-serve-host",
|
||||
StorageServeBasePath: "storage-serve-base-path",
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,5 +309,11 @@ func GetEnvNames() Flags {
|
||||
StorageServeProtocol: "GTS_STORAGE_SERVE_PROTOCOL",
|
||||
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",
|
||||
StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS",
|
||||
StatusesMaxMediaFiles: "GTS_STATUSES_MAX_MEDIA_FILES",
|
||||
}
|
||||
}
|
||||
|
33
internal/config/statuses.go
Normal file
33
internal/config/statuses.go
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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"`
|
||||
// Maximum amount of characters allowed in a content-warning/spoiler field
|
||||
CWMaxChars int `yaml:"cw_max_chars"`
|
||||
// Maximum number of options allowed in a poll
|
||||
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"`
|
||||
}
|
@ -45,7 +45,10 @@ type Status struct {
|
||||
// cw string for this status
|
||||
ContentWarning string
|
||||
// visibility entry for this status
|
||||
Visibility *Visibility
|
||||
Visibility *Visibility
|
||||
// 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.
|
||||
|
@ -18,29 +18,6 @@
|
||||
|
||||
package mastotypes
|
||||
|
||||
// StatusRequest represents a mastodon-api status POST request, as defined here: https://docs.joinmastodon.org/methods/statuses/
|
||||
// It should be used at the path https://mastodon.example/api/v1/statuses
|
||||
type StatusRequest struct {
|
||||
// Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
|
||||
Status string `form:"status"`
|
||||
// Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used.
|
||||
MediaIDs []string `form:"media_ids"`
|
||||
// Poll to include with this status.
|
||||
Poll *PollRequest `form:"poll"`
|
||||
// ID of the status being replied to, if status is a reply
|
||||
InReplyToID string `form:"in_reply_to_id"`
|
||||
// Mark status and attached media as sensitive?
|
||||
Sensitive bool `form:"sensitive"`
|
||||
// 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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// Status represents a mastodon-api Status type, as defined here: https://docs.joinmastodon.org/entities/status/
|
||||
type Status struct {
|
||||
// ID of the status in the database.
|
||||
@ -108,3 +85,26 @@ type Status struct {
|
||||
// the original text from the HTML content.
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// StatusCreateRequest represents a mastodon-api status POST request, as defined here: https://docs.joinmastodon.org/methods/statuses/
|
||||
// It should be used at the path https://mastodon.example/api/v1/statuses
|
||||
type StatusCreateRequest struct {
|
||||
// Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
|
||||
Status string `form:"status"`
|
||||
// Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used.
|
||||
MediaIDs []string `form:"media_ids"`
|
||||
// Poll to include with this status.
|
||||
Poll *PollRequest `form:"poll"`
|
||||
// ID of the status being replied to, if status is a reply
|
||||
InReplyToID string `form:"in_reply_to_id"`
|
||||
// Mark status and attached media as sensitive?
|
||||
Sensitive bool `form:"sensitive"`
|
||||
// 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"`
|
||||
// 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"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user