diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 9482152..a113ef2 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -24,6 +24,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/action" + "github.com/superseriousbusiness/gotosocial/internal/clitools/admin/account" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" @@ -263,6 +264,104 @@ func main() { }, }, }, + { + Name: "admin", + Usage: "gotosocial admin-related tasks", + Subcommands: []*cli.Command{ + { + Name: "account", + Usage: "admin commands related to accounts", + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "create a new account", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + &cli.StringFlag{ + Name: config.EmailFlag, + Usage: config.EmailUsage, + }, + &cli.StringFlag{ + Name: config.PasswordFlag, + Usage: config.PasswordUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Create) + }, + }, + { + Name: "confirm", + Usage: "confirm an existing account manually, thereby skipping email confirmation", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Confirm) + }, + }, + { + Name: "promote", + Usage: "promote an account to admin", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Promote) + }, + }, + { + Name: "demote", + Usage: "demote an account from admin to normal user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Demote) + }, + }, + { + Name: "disable", + Usage: "prevent an account from signing in or posting etc, but don't delete anything", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Disable) + }, + }, + { + Name: "suspend", + Usage: "completely remove an account and all of its posts, media, etc", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: config.UsernameFlag, + Usage: config.UsernameUsage, + }, + }, + Action: func(c *cli.Context) error { + return runAction(c, account.Suspend) + }, + }, + }, + }, + }, + }, { Name: "db", Usage: "database-related tasks and utils", @@ -308,7 +407,9 @@ func runAction(c *cli.Context, a action.GTSAction) error { return fmt.Errorf("error creating config: %s", err) } // ... and the flags set on the *cli.Context by urfave - conf.ParseCLIFlags(c) + if err := conf.ParseCLIFlags(c); err != nil { + return fmt.Errorf("error parsing config: %s", err) + } // create a logger with the log level, formatting, and output splitter already set log, err := log.New(conf.LogLevel) diff --git a/internal/clitools/admin/account/account.go b/internal/clitools/admin/account/account.go new file mode 100644 index 0000000..da4de47 --- /dev/null +++ b/internal/clitools/admin/account/account.go @@ -0,0 +1,209 @@ +/* + 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 account + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/action" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// Create creates a new account in the database using the provided flags. +var Create action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + dbConn, err := pg.NewPostgresService(ctx, c, log) + if err != nil { + return fmt.Errorf("error creating dbservice: %s", err) + } + + username, ok := c.AccountCLIFlags[config.UsernameFlag] + if !ok { + return errors.New("no username set") + } + if err := util.ValidateUsername(username); err != nil { + return err + } + + email, ok := c.AccountCLIFlags[config.EmailFlag] + if !ok { + return errors.New("no email set") + } + if err := util.ValidateEmail(email); err != nil { + return err + } + + password, ok := c.AccountCLIFlags[config.PasswordFlag] + if !ok { + return errors.New("no password set") + } + if err := util.ValidateNewPassword(password); err != nil { + return err + } + + _, err = dbConn.NewSignup(username, "", false, email, password, nil, "", "") + if err != nil { + return err + } + + return dbConn.Stop(ctx) +} + +// Confirm sets a user to Approved, sets Email to the current UnconfirmedEmail value, and sets ConfirmedAt to now. +var Confirm action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + dbConn, err := pg.NewPostgresService(ctx, c, log) + if err != nil { + return fmt.Errorf("error creating dbservice: %s", err) + } + + username, ok := c.AccountCLIFlags[config.UsernameFlag] + if !ok { + return errors.New("no username set") + } + if err := util.ValidateUsername(username); err != nil { + return err + } + + a := >smodel.Account{} + if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + return err + } + + u := >smodel.User{} + if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil { + return err + } + + u.Approved = true + u.Email = u.UnconfirmedEmail + u.ConfirmedAt = time.Now() + if err := dbConn.UpdateByID(u.ID, u); err != nil { + return err + } + + return dbConn.Stop(ctx) +} + +// Promote sets a user to admin. +var Promote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + dbConn, err := pg.NewPostgresService(ctx, c, log) + if err != nil { + return fmt.Errorf("error creating dbservice: %s", err) + } + + username, ok := c.AccountCLIFlags[config.UsernameFlag] + if !ok { + return errors.New("no username set") + } + if err := util.ValidateUsername(username); err != nil { + return err + } + + a := >smodel.Account{} + if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + return err + } + + u := >smodel.User{} + if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil { + return err + } + u.Admin = true + if err := dbConn.UpdateByID(u.ID, u); err != nil { + return err + } + + return dbConn.Stop(ctx) +} + +// Demote sets admin on a user to false. +var Demote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + dbConn, err := pg.NewPostgresService(ctx, c, log) + if err != nil { + return fmt.Errorf("error creating dbservice: %s", err) + } + + username, ok := c.AccountCLIFlags[config.UsernameFlag] + if !ok { + return errors.New("no username set") + } + if err := util.ValidateUsername(username); err != nil { + return err + } + + a := >smodel.Account{} + if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + return err + } + + u := >smodel.User{} + if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil { + return err + } + u.Admin = false + if err := dbConn.UpdateByID(u.ID, u); err != nil { + return err + } + + return dbConn.Stop(ctx) +} + +// Disable sets Disabled to true on a user. +var Disable action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + dbConn, err := pg.NewPostgresService(ctx, c, log) + if err != nil { + return fmt.Errorf("error creating dbservice: %s", err) + } + + username, ok := c.AccountCLIFlags[config.UsernameFlag] + if !ok { + return errors.New("no username set") + } + if err := util.ValidateUsername(username); err != nil { + return err + } + + a := >smodel.Account{} + if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + return err + } + + u := >smodel.User{} + if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil { + return err + } + u.Disabled = true + if err := dbConn.UpdateByID(u.ID, u); err != nil { + return err + } + + return dbConn.Stop(ctx) +} + +var Suspend action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { + // TODO + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 23f7d0d..c06f453 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,14 +19,31 @@ package config import ( + "errors" "fmt" "os" "gopkg.in/yaml.v2" ) +const ( + UsernameFlag = "username" + UsernameUsage = "the username to create/delete/etc" + + EmailFlag = "email" + EmailUsage = "the email address of this account" + + PasswordFlag = "password" + PasswordUsage = "the password to set for this account" +) + // Config pulls together all the configuration needed to run gotosocial type Config struct { + /* + Parseable from .yaml configuration file. + For long-running commands (server start etc). + */ + LogLevel string `yaml:"logLevel"` ApplicationName string `yaml:"applicationName"` Host string `yaml:"host"` @@ -38,6 +55,12 @@ type Config struct { StorageConfig *StorageConfig `yaml:"storage"` StatusesConfig *StatusesConfig `yaml:"statuses"` LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"` + + /* + Not parsed from .yaml configuration file. + For short running commands (admin CLI tools etc). + */ + AccountCLIFlags map[string]string } // FromFile returns a new config from a file, or an error if something goes amiss. @@ -62,6 +85,7 @@ func Empty() *Config { StorageConfig: &StorageConfig{}, StatusesConfig: &StatusesConfig{}, LetsEncryptConfig: &LetsEncryptConfig{}, + AccountCLIFlags: make(map[string]string), } } @@ -81,7 +105,7 @@ func loadFromFile(path string) (*Config, error) { } // ParseCLIFlags sets flags on the config using the provided Flags object -func (c *Config) ParseCLIFlags(f KeyedFlags) { +func (c *Config) ParseCLIFlags(f KeyedFlags) error { fn := GetFlagNames() // For all of these flags, we only want to set them on the config if: @@ -104,10 +128,16 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) { if c.Host == "" || f.IsSet(fn.Host) { c.Host = f.String(fn.Host) } + if c.Host == "" { + return errors.New("host was not set") + } if c.Protocol == "" || f.IsSet(fn.Protocol) { c.Protocol = f.String(fn.Protocol) } + if c.Protocol == "" { + return errors.New("protocol was not set") + } // db flags if c.DBConfig.Type == "" || f.IsSet(fn.DbType) { @@ -215,6 +245,15 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) { if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) { c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress) } + + // command-specific flags + + // admin account CLI flags + c.AccountCLIFlags[UsernameFlag] = f.String(UsernameFlag) + c.AccountCLIFlags[EmailFlag] = f.String(EmailFlag) + c.AccountCLIFlags[PasswordFlag] = f.String(PasswordFlag) + + return nil } // KeyedFlags is a wrapper for any type that can store keyed flags and give them back. diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go index e229dcf..5d02836 100644 --- a/internal/message/frprocess.go +++ b/internal/message/frprocess.go @@ -54,9 +54,22 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap return nil, NewErrorNotFound(err) } + originAccount := >smodel.Account{} + if err := p.db.GetByID(follow.AccountID, originAccount); err != nil { + return nil, NewErrorInternalError(err) + } + + targetAccount := >smodel.Account{} + if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil { + return nil, NewErrorInternalError(err) + } + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsFollow, APActivityType: gtsmodel.ActivityStreamsAccept, GTSModel: follow, + OriginAccount: originAccount, + TargetAccount: targetAccount, } gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)