website/cmd/nonshy/main.go
Noah Petherbridge b8be14ea8d Search By Location
* 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.
2024-08-03 14:54:22 -07:00

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"))
}