all repos

rss-tools @ a5ac527

get rss feed from sources that(i need and) dont provide one

rss-tools/app/app.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix logger, 10 days ago
1
package app
2
3
import (
4
	"context"
5
	"fmt"
6
	"log/slog"
7
	"net/http"
8
	"os"
9
	"strings"
10
	"sync"
11
	"time"
12
13
	"go.etcd.io/bbolt"
14
)
15
16
type App struct {
17
	mux           *http.ServeMux
18
	workers       []func(context.Context) error
19
	shutdowns     []func(context.Context)
20
	wg            *sync.WaitGroup
21
	db            *bbolt.DB
22
	scraperClient *http.Client
23
24
	// TODO: cacher, each scrapper should be able to get it's own cacher
25
	Config *Config
26
	Client *http.Client
27
	Logger *slog.Logger
28
}
29
30
func New(cfg *Config, db *bbolt.DB) *App {
31
	return &App{
32
		mux:           http.NewServeMux(),
33
		workers:       nil,
34
		wg:            &sync.WaitGroup{},
35
		db:            db,
36
		scraperClient: &http.Client{Timeout: 10 * time.Second},
37
38
		Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})),
39
		Client: &http.Client{Timeout: 31 * time.Second},
40
		Config: cfg,
41
	}
42
}
43
44
// Route registers a global route. pattern syntax is the same as in [http.ServeMux].HandlerFunc
45
func (a App) Route(pattern string, handler http.HandlerFunc) {
46
	a.mux.HandleFunc(pattern, handler)
47
}
48
49
// AddWorker adds background worker
50
func (a *App) AddWorker(worker func(ctx context.Context) error) {
51
	a.workers = append(a.workers, worker)
52
}
53
54
// AddShutdown registers a shutdown hook that will be called when the app stops.
55
// Shutdown hooks are called in reverse order of registration.
56
func (a *App) AddShutdown(fn func(ctx context.Context)) {
57
	a.shutdowns = append(a.shutdowns, fn)
58
}
59
60
const (
61
	defaultScraperUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
62
	defaultScraperAccept    = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
63
)
64
65
// Get is intended for scraping sources; API SDK calls should use [App.Client] directly.
66
func (a *App) Get(ctx context.Context, url string) (*http.Response, error) {
67
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
68
	if err != nil {
69
		return nil, err
70
	}
71
	req.Header.Set("User-Agent", defaultScraperUserAgent)
72
	req.Header.Set("Accept", defaultScraperAccept)
73
	return a.scraperClient.Do(req)
74
}
75
76
// Start starts an app and with all it's registered sources
77
func (a *App) Start(ctx context.Context) error {
78
	// workers
79
	for _, worker := range a.workers {
80
		a.wg.Add(1)
81
		go func(w func(context.Context) error) {
82
			defer a.wg.Done()
83
			if err := w(ctx); err != nil {
84
				a.Logger.ErrorContext(ctx, "worker exited with an error", "err", err)
85
			}
86
		}(worker)
87
	}
88
89
	// http server
90
	handler := a.recoverMiddleware(a.mux)
91
	handler = a.loggingMiddleware(handler)
92
	if strings.TrimSpace(a.Config.AuthToken) != "" {
93
		handler = a.authMiddleware(handler)
94
	}
95
96
	httpSrv := &http.Server{
97
		Addr:    fmt.Sprintf(":%d", a.Config.Port), // fixme
98
		Handler: handler,
99
	}
100
101
	go func() {
102
		go func() {
103
			<-ctx.Done()
104
			shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
105
			defer cancel()
106
			httpSrv.Shutdown(shutdownCtx)
107
		}()
108
	}()
109
110
	a.Logger.Info("starting http server", "port", a.Config.Port)
111
	if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed {
112
		return err
113
	}
114
115
	a.wg.Wait()
116
117
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
118
	defer cancel()
119
	for _, fn := range a.shutdowns {
120
		fn(shutdownCtx)
121
	}
122
123
	return nil
124
}