287 lines
6.7 KiB
Go
287 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.quad4.io/Quad4-Software/webnews/internal/api"
|
|
"git.quad4.io/Quad4-Software/webnews/internal/storage"
|
|
)
|
|
|
|
// App struct
|
|
type App struct {
|
|
ctx context.Context
|
|
am *api.AuthManager
|
|
db *storage.SQLiteDB
|
|
port int
|
|
debug bool
|
|
}
|
|
|
|
// NewApp creates a new App struct
|
|
func NewApp(debug bool) *App {
|
|
// Initialize SQLite in the user's home directory
|
|
home, err := os.UserHomeDir()
|
|
dbPath := "webnews.db"
|
|
if err == nil {
|
|
dbPath = filepath.Join(home, ".config", "webnews", "library.db")
|
|
}
|
|
|
|
if debug {
|
|
fmt.Printf("[debug] using database path: %s\n", dbPath)
|
|
}
|
|
|
|
db, err := storage.NewSQLiteDB(dbPath)
|
|
if err != nil {
|
|
fmt.Printf("Warning: Failed to initialize SQLite, falling back to IndexedDB: %v\n", err)
|
|
} else if debug {
|
|
fmt.Printf("[debug] SQLite initialized\n")
|
|
}
|
|
|
|
return &App{
|
|
am: api.NewAuthManager("none", "", "", false),
|
|
db: db,
|
|
debug: debug,
|
|
}
|
|
}
|
|
|
|
func (a *App) logDebug(format string, args ...any) {
|
|
if a != nil && a.debug {
|
|
fmt.Printf("[debug] "+format+"\n", args...)
|
|
}
|
|
}
|
|
|
|
// logHandler wraps HTTP handlers to log requests when debug is enabled.
|
|
func (a *App) logHandler(next http.Handler) http.Handler {
|
|
if !a.debug {
|
|
return next
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
next.ServeHTTP(w, r)
|
|
fmt.Printf("[debug] http %s %s %dms\n", r.Method, r.URL.Path, time.Since(start).Milliseconds())
|
|
})
|
|
}
|
|
|
|
// GetDBStats returns database statistics
|
|
func (a *App) GetDBStats() (storage.DBStats, error) {
|
|
a.logDebug("GetDBStats")
|
|
if a.db == nil {
|
|
return storage.DBStats{}, fmt.Errorf("SQLite not initialized")
|
|
}
|
|
return a.db.GetStats()
|
|
}
|
|
|
|
// VacuumDB runs the VACUUM command
|
|
func (a *App) VacuumDB() error {
|
|
a.logDebug("VacuumDB")
|
|
if a.db == nil {
|
|
return fmt.Errorf("SQLite not initialized")
|
|
}
|
|
return a.db.Vacuum()
|
|
}
|
|
|
|
// CheckDBIntegrity runs a PRAGMA integrity_check
|
|
func (a *App) CheckDBIntegrity() (string, error) {
|
|
a.logDebug("CheckDBIntegrity")
|
|
if a.db == nil {
|
|
return "", fmt.Errorf("SQLite not initialized")
|
|
}
|
|
return a.db.IntegrityCheck()
|
|
}
|
|
|
|
// Storage methods for Wails to call
|
|
func (a *App) SaveSettings(settings string) error {
|
|
a.logDebug("SaveSettings")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.SaveSettings(settings)
|
|
}
|
|
|
|
func (a *App) GetSettings() (string, error) {
|
|
a.logDebug("GetSettings")
|
|
if a.db == nil {
|
|
return "{}", nil
|
|
}
|
|
return a.db.GetSettings()
|
|
}
|
|
|
|
func (a *App) SaveCategories(cats string) error {
|
|
a.logDebug("SaveCategories")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.SaveCategories(cats)
|
|
}
|
|
|
|
func (a *App) GetCategories() (string, error) {
|
|
a.logDebug("GetCategories")
|
|
if a.db == nil {
|
|
return "[]", nil
|
|
}
|
|
return a.db.GetCategories()
|
|
}
|
|
|
|
func (a *App) SaveFeeds(feeds string) error {
|
|
a.logDebug("SaveFeeds")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.SaveFeeds(feeds)
|
|
}
|
|
|
|
func (a *App) GetFeeds() (string, error) {
|
|
a.logDebug("GetFeeds")
|
|
if a.db == nil {
|
|
return "[]", nil
|
|
}
|
|
return a.db.GetFeeds()
|
|
}
|
|
|
|
func (a *App) SaveArticles(articles string) error {
|
|
a.logDebug("SaveArticles")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.SaveArticles(articles)
|
|
}
|
|
|
|
func (a *App) GetArticles(feedId string, offset, limit int) (string, error) {
|
|
a.logDebug("GetArticles feedId=%s offset=%d limit=%d", feedId, offset, limit)
|
|
if a.db == nil {
|
|
return "[]", nil
|
|
}
|
|
return a.db.GetArticles(feedId, offset, limit)
|
|
}
|
|
|
|
func (a *App) SearchArticles(query string, limit int) (string, error) {
|
|
a.logDebug("SearchArticles query=%s limit=%d", query, limit)
|
|
if a.db == nil {
|
|
return "[]", nil
|
|
}
|
|
return a.db.SearchArticles(query, limit)
|
|
}
|
|
|
|
func (a *App) UpdateArticle(article string) error {
|
|
a.logDebug("UpdateArticle")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.UpdateArticle(article)
|
|
}
|
|
|
|
func (a *App) MarkAsRead(id string) error {
|
|
a.logDebug("MarkAsRead id=%s", id)
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.MarkAsRead(id)
|
|
}
|
|
|
|
func (a *App) DeleteFeed(feedId string) error {
|
|
a.logDebug("DeleteFeed feedId=%s", feedId)
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.DeleteFeed(feedId)
|
|
}
|
|
|
|
func (a *App) PurgeOldContent(days int) (int64, error) {
|
|
a.logDebug("PurgeOldContent days=%d", days)
|
|
if a.db == nil {
|
|
return 0, nil
|
|
}
|
|
return a.db.PurgeOldContent(days)
|
|
}
|
|
|
|
func (a *App) ClearAll() error {
|
|
a.logDebug("ClearAll")
|
|
if a.db == nil {
|
|
return nil
|
|
}
|
|
return a.db.ClearAll()
|
|
}
|
|
|
|
func (a *App) GetReadingHistory(days int) (string, error) {
|
|
a.logDebug("GetReadingHistory days=%d", days)
|
|
if a.db == nil {
|
|
return "[]", nil
|
|
}
|
|
return a.db.GetReadingHistory(days)
|
|
}
|
|
|
|
// startup is called when the app starts. The context is saved
|
|
// so we can call the runtime methods
|
|
func (a *App) startup(ctx context.Context) {
|
|
a.ctx = ctx
|
|
a.logDebug("startup begin")
|
|
|
|
// Start local API server on a random port
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
fmt.Printf("Error starting local server: %v\n", err)
|
|
return
|
|
}
|
|
|
|
a.port = listener.Addr().(*net.TCPAddr).Port
|
|
a.logDebug("local API listener bound on %s", listener.Addr().String())
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// CORS middleware for local desktop API
|
|
cors := func(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Account-Number")
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// Register handlers from our common api package
|
|
mux.HandleFunc("/api/feed", cors(api.HandleFeedProxy))
|
|
mux.HandleFunc("/api/proxy", cors(api.HandleProxy))
|
|
mux.HandleFunc("/api/fulltext", cors(api.HandleFullText))
|
|
mux.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprintf(w, `{"status":"ok","auth":{"required":false,"mode":"none","canReg":false}}`)
|
|
}))
|
|
|
|
server := &http.Server{
|
|
Addr: listener.Addr().String(),
|
|
Handler: a.logHandler(mux),
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
fmt.Printf("Error serving desktop API: %v\n", err)
|
|
}
|
|
}()
|
|
fmt.Printf("Desktop API server started on port %d\n", a.port)
|
|
a.logDebug("startup complete")
|
|
}
|
|
|
|
// GetAPIPort returns the port the local server is running on
|
|
// This can be used by the frontend to know where to send requests
|
|
func (a *App) GetAPIPort() int {
|
|
a.logDebug("GetAPIPort -> %d", a.port)
|
|
return a.port
|
|
}
|
|
|
|
// LogFrontend allows the frontend to log to the terminal
|
|
func (a *App) LogFrontend(message string) {
|
|
fmt.Printf("[frontend] %s\n", message)
|
|
}
|