diff --git a/app.go b/app.go index 82548e9..76aae55 100644 --- a/app.go +++ b/app.go @@ -1,121 +1,125 @@ package writefreely import ( "database/sql" "flag" "fmt" _ "github.com/go-sql-driver/mysql" "net/http" "os" "os/signal" "syscall" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" ) const ( staticDir = "static/" + + serverSoftware = "Write Freely" + softwareURL = "https://writefreely.org" + softwareVer = "0.1" ) type app struct { router *mux.Router db *datastore cfg *config.Config keys *keychain sessionStore *sessions.CookieStore } var shttp = http.NewServeMux() func Serve() { createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") flag.Parse() if *createConfig { log.Info("Creating configuration...") c := config.New() log.Info("Saving configuration...") config.Save(c) os.Exit(0) } log.Info("Initializing...") log.Info("Loading configuration...") cfg, err := config.Load() if err != nil { log.Error("Unable to load configuration: %v", err) os.Exit(1) } app := &app{ cfg: cfg, } // Load keys log.Info("Loading encryption keys...") err = initKeys(app) if err != nil { log.Error("\n%s\n", err) } // Initialize modules app.sessionStore = initSession(app) // Check database configuration if app.cfg.Database.User == "" || app.cfg.Database.Password == "" { log.Error("Database user or password not set.") os.Exit(1) } if app.cfg.Database.Host == "" { app.cfg.Database.Host = "localhost" } if app.cfg.Database.Database == "" { app.cfg.Database.Database = "writeas" } log.Info("Connecting to database...") db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database)) if err != nil { log.Error("\n%s\n", err) os.Exit(1) } app.db = &datastore{db} defer shutdown(app) app.db.SetMaxOpenConns(50) r := mux.NewRouter() handler := NewHandler(app.sessionStore) // Handle app routes initRoutes(handler, r, app.cfg, app.db) // Handle static files fs := http.FileServer(http.Dir(staticDir)) shttp.Handle("/", fs) r.PathPrefix("/").Handler(fs) // Handle shutdown c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c log.Info("Shutting down...") shutdown(app) log.Info("Done.") os.Exit(0) }() // Start web application server http.Handle("/", r) log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port) log.Info("---") http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil) } func shutdown(app *app) { log.Info("Closing database connection...") app.db.Close() } diff --git a/config/config.go b/config/config.go index 630dfe3..77a1f58 100644 --- a/config/config.go +++ b/config/config.go @@ -1,87 +1,90 @@ package config import ( "gopkg.in/ini.v1" ) const ( configFile = "config.ini" ) type ( ServerCfg struct { Host string `ini:"host"` Port int `ini:"port"` } DatabaseCfg struct { Type string `ini:"type"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` } AppCfg struct { MultiUser bool `ini:"multiuser"` OpenSignups bool `ini:"open_signups"` Federation bool `ini:"federation"` + PublicStats bool `ini:"public_stats"` + Private bool `ini:"private"` Name string `ini:"site_name"` JSDisabled bool `ini:"disable_js"` // User registration MinUsernameLen int `ini:"min_username_len"` } Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` } ) func New() *Config { return &Config{ Server: ServerCfg{ Host: "http://localhost:8080", Port: 8080, }, Database: DatabaseCfg{ Type: "mysql", Host: "localhost", Port: 3306, }, App: AppCfg{ Federation: true, + PublicStats: true, MinUsernameLen: 3, }, } } func Load() (*Config, error) { cfg, err := ini.Load(configFile) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } return uc, nil } func Save(uc *Config) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } return cfg.SaveTo(configFile) } diff --git a/nodeinfo.go b/nodeinfo.go new file mode 100644 index 0000000..ae50e9f --- /dev/null +++ b/nodeinfo.go @@ -0,0 +1,87 @@ +package writefreely + +import ( + "fmt" + "github.com/writeas/go-nodeinfo" + "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/config" +) + +type nodeInfoResolver struct { + cfg *config.Config + db *datastore +} + +func nodeInfoConfig(cfg *config.Config) *nodeinfo.Config { + name := cfg.App.SiteName + return &nodeinfo.Config{ + BaseURL: cfg.Server.Host, + InfoURL: "/api/nodeinfo", + + Metadata: nodeinfo.Metadata{ + NodeName: name, + NodeDescription: "Minimal, federated blogging platform.", + Private: cfg.App.Private, + Software: nodeinfo.SoftwareMeta{ + HomePage: softwareURL, + GitHub: "https://github.com/writeas/writefreely", + Follow: "https://writing.exchange/@write_as", + }, + }, + Protocols: []nodeinfo.NodeProtocol{ + nodeinfo.ProtocolActivityPub, + }, + Services: nodeinfo.Services{ + Inbound: []nodeinfo.NodeService{}, + Outbound: []nodeinfo.NodeService{}, + }, + Software: nodeinfo.SoftwareInfo{ + Name: serverSoftware, + Version: softwareVer, + }, + } +} + +func (r nodeInfoResolver) IsOpenRegistration() (bool, error) { + return !r.cfg.App.Private, nil +} + +func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) { + var collCount, postCount, activeHalfYear, activeMonth int + err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) + if err != nil { + collCount = 0 + } + err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) + if err != nil { + log.Error("Unable to fetch post counts: %v", err) + } + + if r.cfg.App.PublicStats { + // Display bi-yearly / monthly stats + err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM ( +SELECT DISTINCT collection_id +FROM posts +INNER JOIN collections c +ON collection_id = c.id +WHERE collection_id IS NOT NULL + AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`, CollPublic)).Scan(&activeHalfYear) + + err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM ( +SELECT DISTINCT collection_id +FROM posts +INNER JOIN FROM collections c +ON collection_id = c.id +WHERE collection_id IS NOT NULL + AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`, CollPublic)).Scan(&activeMonth) + } + + return nodeinfo.Usage{ + Users: nodeinfo.UsageUsers{ + Total: collCount, + ActiveHalfYear: activeHalfYear, + ActiveMonth: activeMonth, + }, + LocalPosts: postCount, + }, nil +} diff --git a/routes.go b/routes.go index 545fc78..45e3be9 100644 --- a/routes.go +++ b/routes.go @@ -1,31 +1,41 @@ package writefreely import ( "github.com/gorilla/mux" + "github.com/writeas/go-nodeinfo" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" + "net/http" "strings" ) func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { isSingleUser := !cfg.App.MultiUser // Write.as router hostSubroute := cfg.Server.Host[strings.Index(cfg.Server.Host, "://")+3:] if isSingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { hostSubroute = "localhost" } } if isSingleUser { log.Info("Adding %s routes (single user)...", hostSubroute) return } // Primary app routes log.Info("Adding %s routes (multi-user)...", hostSubroute) + write := r.Host(hostSubroute).Subrouter() + + // Federation endpoints + // nodeinfo + niCfg := nodeInfoConfig(cfg) + ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db}) + write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) + write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) }