b8be14ea8d
* Add a world cities database with type-ahead search on the Member Directory. * Users can search for a known city to order users by distance from that city rather than from their own configured location on their settings page. * Users must opt-in their own location before this feature may be used, in order to increase adoption of the location feature and to enforce fairness. * The `nonshy setup locations` command can import the world cities database.
329 lines
7.9 KiB
Go
329 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
nonshy "code.nonshy.com/nonshy/website/pkg"
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/encryption/coldstorage"
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
"code.nonshy.com/nonshy/website/pkg/models/backfill"
|
|
"code.nonshy.com/nonshy/website/pkg/models/exporting"
|
|
"code.nonshy.com/nonshy/website/pkg/redis"
|
|
"code.nonshy.com/nonshy/website/pkg/worker"
|
|
"github.com/urfave/cli/v2"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// Build-time values.
|
|
var (
|
|
Build = "n/a"
|
|
BuildDate = "n/a"
|
|
)
|
|
|
|
func init() {
|
|
config.RuntimeVersion = nonshy.Version
|
|
config.RuntimeBuild = Build
|
|
config.RuntimeBuildDate = BuildDate
|
|
}
|
|
|
|
func main() {
|
|
app := &cli.App{
|
|
Name: "nonshy",
|
|
Usage: "a niche social networking webapp",
|
|
Commands: []*cli.Command{
|
|
{
|
|
Name: "web",
|
|
Usage: "start the web server",
|
|
Flags: []cli.Flag{
|
|
// Debug mode.
|
|
&cli.BoolFlag{
|
|
Name: "debug",
|
|
Aliases: []string{"d"},
|
|
Usage: "debug mode (logging and reloading templates)",
|
|
},
|
|
|
|
// HTTP settings.
|
|
&cli.StringFlag{
|
|
Name: "host",
|
|
Aliases: []string{"H"},
|
|
Value: "0.0.0.0",
|
|
Usage: "host address to listen on",
|
|
},
|
|
&cli.IntFlag{
|
|
Name: "port",
|
|
Aliases: []string{"P"},
|
|
Value: 8080,
|
|
Usage: "port number to listen on",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("debug") {
|
|
config.Debug = true
|
|
log.SetDebug(true)
|
|
}
|
|
|
|
initdb(c)
|
|
initcache(c)
|
|
|
|
log.Debug("Debug logging enabled.")
|
|
|
|
app := &nonshy.WebServer{
|
|
Host: c.String("host"),
|
|
Port: c.Int("port"),
|
|
}
|
|
|
|
// Kick off background worker threads.
|
|
go worker.WatchBareRTC()
|
|
|
|
return app.Run()
|
|
},
|
|
},
|
|
{
|
|
Name: "user",
|
|
Usage: "manage user accounts such as to create admins",
|
|
Subcommands: []*cli.Command{
|
|
{
|
|
Name: "add",
|
|
Usage: "add a new user account",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "username",
|
|
Aliases: []string{"u"},
|
|
Required: true,
|
|
Usage: "username, case insensitive",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "email",
|
|
Aliases: []string{"e"},
|
|
Required: true,
|
|
Usage: "email address",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "password",
|
|
Aliases: []string{"p"},
|
|
Required: true,
|
|
Usage: "set user password",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "admin",
|
|
Usage: "set admin status",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
initdb(c)
|
|
|
|
log.Info("Creating user account: %s", c.String("username"))
|
|
user, err := models.CreateUser(
|
|
c.String("username"),
|
|
c.String("email"),
|
|
c.String("password"),
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Making an admin?
|
|
if c.Bool("admin") {
|
|
log.Warn("Promoting user to admin status")
|
|
user.IsAdmin = true
|
|
user.Save()
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "export",
|
|
Usage: "create a data export ZIP from a user's account",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "username",
|
|
Aliases: []string{"u"},
|
|
Required: true,
|
|
Usage: "username or e-mail, case insensitive",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "output",
|
|
Aliases: []string{"o"},
|
|
Required: true,
|
|
Usage: "output file (.zip extension)",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
initdb(c)
|
|
|
|
log.Info("Creating data export for user account: %s", c.String("username"))
|
|
user, err := models.FindUser(c.String("username"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = exporting.ExportUser(user, c.String("output"))
|
|
return err
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "coldstorage",
|
|
Usage: "cold storage functions for sensitive files",
|
|
Subcommands: []*cli.Command{
|
|
{
|
|
Name: "decrypt",
|
|
Usage: "decrypt a file from cold storage using the RSA private key",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "key",
|
|
Aliases: []string{"k"},
|
|
Required: true,
|
|
Usage: "RSA private key file for cold storage",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "aes",
|
|
Aliases: []string{"a"},
|
|
Required: true,
|
|
Usage: "AES key file used with the encrypted item in question (.aes file)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "input",
|
|
Aliases: []string{"i"},
|
|
Required: true,
|
|
Usage: "input file to decrypt (.enc file)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "output",
|
|
Aliases: []string{"o"},
|
|
Required: true,
|
|
Usage: "output file to write to (like a .jpg file)",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
err := coldstorage.FileFromColdStorage(
|
|
c.String("key"),
|
|
c.String("aes"),
|
|
c.String("input"),
|
|
c.String("output"),
|
|
)
|
|
if err != nil {
|
|
log.Error("Error decrypting from cold storage: %s", err)
|
|
return err
|
|
}
|
|
|
|
log.Info("Wrote decrypted file to: %s", c.String("output"))
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "setup",
|
|
Usage: "setup and data import functions for the website",
|
|
Subcommands: []*cli.Command{
|
|
{
|
|
Name: "locations",
|
|
Usage: "import the database of world city locations from simplemaps.com",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "input",
|
|
Aliases: []string{"i"},
|
|
Required: true,
|
|
Usage: "the input worldcities.csv from simplemaps, with required headers: id, city, lat, lng, country, iso2",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
initdb(c)
|
|
|
|
filename := c.String("input")
|
|
return models.InitializeWorldCities(filename)
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "backfill",
|
|
Usage: "One-off maintenance tasks and data backfills for database migrations",
|
|
Subcommands: []*cli.Command{
|
|
{
|
|
Name: "filesizes",
|
|
Usage: "repopulate Filesizes on all photos and comment_photos which have a zero stored in the DB",
|
|
Action: func(c *cli.Context) error {
|
|
initdb(c)
|
|
|
|
log.Info("Running BackfillFilesizes()")
|
|
err := backfill.BackfillFilesizes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "vacuum",
|
|
Usage: "Run database maintenance tasks (clean up broken links, remove orphaned comment photos, etc.) for data consistency.",
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "dryrun",
|
|
Usage: "don't actually delete anything",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
initdb(c)
|
|
return worker.Vacuum(c.Bool("dryrun"))
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := app.Run(os.Args); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func initdb(c *cli.Context) {
|
|
// Load the settings.json
|
|
config.LoadSettings()
|
|
|
|
var gormcfg = &gorm.Config{}
|
|
if c.Bool("debug") {
|
|
gormcfg = &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Info),
|
|
}
|
|
}
|
|
|
|
// Initialize the database.
|
|
log.Info("Initializing DB")
|
|
if config.Current.Database.IsSQLite {
|
|
db, err := gorm.Open(sqlite.Open(config.Current.Database.SQLite), gormcfg)
|
|
if err != nil {
|
|
panic("failed to open SQLite DB")
|
|
}
|
|
models.DB = db
|
|
} else if config.Current.Database.IsPostgres {
|
|
db, err := gorm.Open(postgres.Open(config.Current.Database.Postgres), gormcfg)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to open Postgres DB: %s", err))
|
|
}
|
|
models.DB = db
|
|
} else {
|
|
log.Fatal("A choice of SQL database is required.")
|
|
}
|
|
|
|
// Auto-migrate the DB.
|
|
models.AutoMigrate()
|
|
}
|
|
|
|
func initcache(c *cli.Context) {
|
|
// Initialize Redis.
|
|
log.Info("Initializing Redis")
|
|
redis.Setup(c.String("redis"))
|
|
}
|