chore: refactor
This commit is contained in:
		
							parent
							
								
									303ca5a2db
								
							
						
					
					
						commit
						a3e6fc0c0f
					
				
							
								
								
									
										40
									
								
								app.go
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								app.go
									
									
									
									
									
								
							@ -4,12 +4,10 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"iter"
 | 
					 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"runtime/debug"
 | 
						"runtime/debug"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ "embed"
 | 
						_ "embed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,14 +16,15 @@ import (
 | 
				
			|||||||
	"go.opentelemetry.io/otel/attribute"
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
 | 
						semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
 | 
				
			||||||
	"go.opentelemetry.io/otel/trace"
 | 
						"go.opentelemetry.io/otel/trace"
 | 
				
			||||||
 | 
						"go.sour.is/xt/internal/console"
 | 
				
			||||||
	"go.sour.is/xt/internal/otel"
 | 
						"go.sour.is/xt/internal/otel"
 | 
				
			||||||
	"go.yarn.social/lextwt"
 | 
						"go.yarn.social/lextwt"
 | 
				
			||||||
	"go.yarn.social/types"
 | 
						"go.yarn.social/types"
 | 
				
			||||||
	"golang.org/x/sync/errgroup"
 | 
						"golang.org/x/sync/errgroup"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func run(c *console) error {
 | 
					func run(ctx context.Context, c *console.C[args]) error {
 | 
				
			||||||
	ctx, span := otel.Span(c.Context)
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
	defer span.End()
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	bi, _ := debug.ReadBuildInfo()
 | 
						bi, _ := debug.ReadBuildInfo()
 | 
				
			||||||
@ -33,8 +32,8 @@ func run(c *console) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	a := c.Args()
 | 
						a := c.Args()
 | 
				
			||||||
	app := &appState{
 | 
						app := &appState{
 | 
				
			||||||
		args:  a,
 | 
							args: a,
 | 
				
			||||||
		feeds: sync.Map{},
 | 
							C:    c,
 | 
				
			||||||
		queue: FibHeap(func(a, b *Feed) bool {
 | 
							queue: FibHeap(func(a, b *Feed) bool {
 | 
				
			||||||
			return a.NextScanOn.Time.Before(b.NextScanOn.Time)
 | 
								return a.NextScanOn.Time.Before(b.NextScanOn.Time)
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
@ -96,25 +95,24 @@ func run(c *console) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wg, ctx := errgroup.WithContext(ctx)
 | 
						wg, ctx := errgroup.WithContext(ctx)
 | 
				
			||||||
	c.Context = ctx
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wg.Go(func() error {
 | 
						wg.Go(func() error {
 | 
				
			||||||
		return feedRefreshProcessor(c, app)
 | 
							return feedRefreshProcessor(ctx, app)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	go httpServer(c, app)
 | 
						go httpServer(ctx, app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = wg.Wait()
 | 
						err = wg.Wait()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return c.Context.Err()
 | 
						return ctx.Err()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type appState struct {
 | 
					type appState struct {
 | 
				
			||||||
	args  args
 | 
						args  args
 | 
				
			||||||
	feeds sync.Map
 | 
					 | 
				
			||||||
	queue *fibHeap[Feed]
 | 
						queue *fibHeap[Feed]
 | 
				
			||||||
 | 
						*console.C[args]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type db struct {
 | 
					type db struct {
 | 
				
			||||||
@ -173,23 +171,3 @@ func (app *appState) DB(ctx context.Context) (db, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return db, err
 | 
						return db, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func (app *appState) Feed(feedID string) *Feed {
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (app *appState) Feeds() iter.Seq2[string, *Feed] {
 | 
					 | 
				
			||||||
	return func(yield func(string, *Feed) bool) {
 | 
					 | 
				
			||||||
		app.feeds.Range(func(k, v any) bool {
 | 
					 | 
				
			||||||
			key, _ := k.(string)
 | 
					 | 
				
			||||||
			value, ok := v.(*Feed)
 | 
					 | 
				
			||||||
			if !ok {
 | 
					 | 
				
			||||||
				return false
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if !yield(key, value) {
 | 
					 | 
				
			||||||
				return false
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return true
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								feed.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								feed.go
									
									
									
									
									
								
							@ -134,6 +134,10 @@ var (
 | 
				
			|||||||
			'+'||abs(refresh_rate+cast(random()%30 as int))||' seconds'
 | 
								'+'||abs(refresh_rate+cast(random()%30 as int))||' seconds'
 | 
				
			||||||
		) < datetime(current_timestamp, '+3 minutes')
 | 
							) < datetime(current_timestamp, '+3 minutes')
 | 
				
			||||||
	`
 | 
						`
 | 
				
			||||||
 | 
						permaban = []string{
 | 
				
			||||||
 | 
							"//lublin.se/",
 | 
				
			||||||
 | 
							"//enotty.dk/",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (f *Feed) Create(ctx context.Context, db db) error {
 | 
					func (f *Feed) Create(ctx context.Context, db db) error {
 | 
				
			||||||
@ -370,11 +374,10 @@ func storeFeed(ctx context.Context, db db, f types.TwtFile) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (feed *Feed) MakeHTTPRequest(ctx context.Context) (*http.Request, error) {
 | 
					func (feed *Feed) MakeHTTPRequest(ctx context.Context) (*http.Request, error) {
 | 
				
			||||||
	if strings.Contains(feed.URI, "lublin.se") {
 | 
						for _, host := range permaban {
 | 
				
			||||||
		return nil, fmt.Errorf("%w: permaban: %s", ErrPermanentlyDead, feed.URI)
 | 
							if strings.Contains(feed.URI, host) {
 | 
				
			||||||
	}
 | 
								return nil, fmt.Errorf("%w: permaban: %s", ErrPermanentlyDead, feed.URI)
 | 
				
			||||||
	if strings.Contains(feed.URI, "enotty.dk") {
 | 
							}
 | 
				
			||||||
		return nil, fmt.Errorf("%w: permaban: %s", ErrPermanentlyDead, feed.URI)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req, err := http.NewRequestWithContext(ctx, "GET", feed.URI, nil)
 | 
						req, err := http.NewRequestWithContext(ctx, "GET", feed.URI, nil)
 | 
				
			||||||
@ -392,6 +395,7 @@ func (feed *Feed) MakeHTTPRequest(ctx context.Context) (*http.Request, error) {
 | 
				
			|||||||
		req.Header.Add("If-None-Match", feed.ETag.String)
 | 
							req.Header.Add("If-None-Match", feed.ETag.String)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: this is probably not needed.
 | 
				
			||||||
	if feed.DiscloseFeedURL != "" && feed.DiscloseNick != "" {
 | 
						if feed.DiscloseFeedURL != "" && feed.DiscloseNick != "" {
 | 
				
			||||||
		req.Header.Set("User-Agent", fmt.Sprintf("xt/%s (+%s; @%s)",
 | 
							req.Header.Set("User-Agent", fmt.Sprintf("xt/%s (+%s; @%s)",
 | 
				
			||||||
			feed.Version, feed.DiscloseFeedURL, feed.DiscloseNick))
 | 
								feed.Version, feed.DiscloseFeedURL, feed.DiscloseNick))
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										71
									
								
								http.go
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								http.go
									
									
									
									
									
								
							@ -1,6 +1,7 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
@ -10,14 +11,16 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/trace"
 | 
				
			||||||
	"go.yarn.social/lextwt"
 | 
						"go.yarn.social/lextwt"
 | 
				
			||||||
	"go.yarn.social/types"
 | 
						"go.yarn.social/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"go.sour.is/xt/internal/otel"
 | 
						"go.sour.is/xt/internal/otel"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func httpServer(c *console, app *appState) error {
 | 
					func httpServer(ctx context.Context, app *appState) error {
 | 
				
			||||||
	ctx, span := otel.Span(c)
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
	defer span.End()
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	span.AddEvent("start http server")
 | 
						span.AddEvent("start http server")
 | 
				
			||||||
@ -110,11 +113,11 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
		defer span.End()
 | 
							defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		args := make([]any, 0, 3)
 | 
							args := make([]any, 0, 3)
 | 
				
			||||||
		uriarg := ""
 | 
							uriarg := "1 = 1"
 | 
				
			||||||
		uri := r.URL.Query().Get("uri")
 | 
							uri := r.URL.Query().Get("uri")
 | 
				
			||||||
		if uri != "" {
 | 
							if uri != "" {
 | 
				
			||||||
			feed_id := urlNS.UUID5(uri)
 | 
								feed_id := urlNS.UUID5(uri)
 | 
				
			||||||
			uriarg = "and feed_id = ?"
 | 
								uriarg = "feed_id = ?"
 | 
				
			||||||
			args = append(args, feed_id)
 | 
								args = append(args, feed_id)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -123,11 +126,39 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
			limit = v
 | 
								limit = v
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		offset := 0
 | 
							var offset int64 = 0
 | 
				
			||||||
		if v, ok := strconv.Atoi(r.URL.Query().Get("offset")); ok == nil {
 | 
							if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
				
			||||||
			offset = v
 | 
								offset = v
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		args = append(args, limit, offset)
 | 
					
 | 
				
			||||||
 | 
							var end int64
 | 
				
			||||||
 | 
							err = db.QueryRowContext(ctx, `
 | 
				
			||||||
 | 
								select count(*) n from twts 
 | 
				
			||||||
 | 
								JOIN (
 | 
				
			||||||
 | 
									SELECT
 | 
				
			||||||
 | 
										feed_id,
 | 
				
			||||||
 | 
										nick,
 | 
				
			||||||
 | 
										uri
 | 
				
			||||||
 | 
									FROM feeds
 | 
				
			||||||
 | 
									where state not in ('frozen', 'permanantly-dead') and `+uriarg+`
 | 
				
			||||||
 | 
								) using (feed_id)
 | 
				
			||||||
 | 
								where `+uriarg, args...).Scan(&end)
 | 
				
			||||||
 | 
							span.RecordError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if offset < 1 {
 | 
				
			||||||
 | 
								offset += end
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							limit = min(100, max(1, limit))
 | 
				
			||||||
 | 
							offset = max(1, offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							args = append(args, limit, offset-int64(limit))
 | 
				
			||||||
 | 
							span.AddEvent("twts", trace.WithAttributes(
 | 
				
			||||||
 | 
								attribute.Int("limit", limit),
 | 
				
			||||||
 | 
								attribute.Int64("offset-end", offset),
 | 
				
			||||||
 | 
								attribute.Int64("offset-start", offset-int64(limit)),
 | 
				
			||||||
 | 
								attribute.Int64("max", end),
 | 
				
			||||||
 | 
							))
 | 
				
			||||||
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
							w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
		rows, err := db.QueryContext(
 | 
							rows, err := db.QueryContext(
 | 
				
			||||||
			ctx,
 | 
								ctx,
 | 
				
			||||||
@ -145,10 +176,9 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
							nick,
 | 
												nick,
 | 
				
			||||||
							uri
 | 
												uri
 | 
				
			||||||
						FROM feeds
 | 
											FROM feeds
 | 
				
			||||||
						where state not in ('frozen', 'permanantly-dead')
 | 
											where state not in ('frozen', 'permanantly-dead') and `+uriarg+`
 | 
				
			||||||
						`+uriarg+`
 | 
					 | 
				
			||||||
					) using (feed_id)
 | 
										) using (feed_id)
 | 
				
			||||||
					order by ulid desc
 | 
										order by ulid asc
 | 
				
			||||||
					limit ?
 | 
										limit ?
 | 
				
			||||||
					offset ?
 | 
										offset ?
 | 
				
			||||||
					`, args...,
 | 
										`, args...,
 | 
				
			||||||
@ -182,10 +212,14 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		var preamble lextwt.Comments
 | 
							var preamble lextwt.Comments
 | 
				
			||||||
		preamble = append(preamble, lextwt.NewComment("# I am the Watcher. I am your guide through this vast new twtiverse."))
 | 
							preamble = append(preamble, lextwt.NewComment("# I am the Watcher. I am your guide through this vast new twtiverse."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							preamble = append(preamble, lextwt.NewComment(fmt.Sprint("# range = 1 ", end)))
 | 
				
			||||||
		preamble = append(preamble, lextwt.NewComment("# self = /api/plain/twts"+mkqry(uri, limit, offset)))
 | 
							preamble = append(preamble, lextwt.NewComment("# self = /api/plain/twts"+mkqry(uri, limit, offset)))
 | 
				
			||||||
		preamble = append(preamble, lextwt.NewComment("# next = /api/plain/twts"+mkqry(uri, limit, offset+len(twts))))
 | 
							if next := offset + int64(len(twts)); next < end {
 | 
				
			||||||
		if offset > 0 {
 | 
								preamble = append(preamble, lextwt.NewComment("# next = /api/plain/twts"+mkqry(uri, limit, next)))
 | 
				
			||||||
			preamble = append(preamble, lextwt.NewComment("# prev = /api/plain/twts"+mkqry(uri, limit, offset-limit)))
 | 
							}
 | 
				
			||||||
 | 
							if prev := offset - int64(limit); prev > 0 {
 | 
				
			||||||
 | 
								preamble = append(preamble, lextwt.NewComment("# prev = /api/plain/twts"+mkqry(uri, limit, prev)))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
							reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
				
			||||||
@ -221,7 +255,7 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
				) using (feed_id)
 | 
									) using (feed_id)
 | 
				
			||||||
				`+where+`
 | 
									`+where+`
 | 
				
			||||||
				order by nick, uri
 | 
									order by nick, uri
 | 
				
			||||||
            `,args...,
 | 
					            `, args...,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			span.RecordError(err)
 | 
								span.RecordError(err)
 | 
				
			||||||
@ -268,7 +302,7 @@ func httpServer(c *console, app *appState) error {
 | 
				
			|||||||
		Handler: http.DefaultServeMux,
 | 
							Handler: http.DefaultServeMux,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.AddCancel(srv.Shutdown)
 | 
						app.AddCancel(srv.Shutdown)
 | 
				
			||||||
	err = srv.ListenAndServe()
 | 
						err = srv.ListenAndServe()
 | 
				
			||||||
	if !errors.Is(err, http.ErrServerClosed) {
 | 
						if !errors.Is(err, http.ErrServerClosed) {
 | 
				
			||||||
		span.RecordError(err)
 | 
							span.RecordError(err)
 | 
				
			||||||
@ -287,20 +321,17 @@ func notAny(s string, chars string) bool {
 | 
				
			|||||||
	return true
 | 
						return true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mkqry(uri string, limit int, offset int64) string {
 | 
				
			||||||
func mkqry(uri string, limit, offset int) string {
 | 
					 | 
				
			||||||
	qry := make([]string, 0, 3)
 | 
						qry := make([]string, 0, 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if uri != "" {
 | 
						if uri != "" {
 | 
				
			||||||
		qry = append(qry, "uri=" + uri)
 | 
							qry = append(qry, "uri="+uri)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	limit = min(100, max(1, limit))
 | 
					 | 
				
			||||||
	if limit != 100 {
 | 
						if limit != 100 {
 | 
				
			||||||
		qry = append(qry, fmt.Sprint("limit=", limit))
 | 
							qry = append(qry, fmt.Sprint("limit=", limit))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	offset = max(0, offset)
 | 
					 | 
				
			||||||
	if offset != 0 {
 | 
						if offset != 0 {
 | 
				
			||||||
		qry = append(qry, fmt.Sprint("offset=", offset))
 | 
							qry = append(qry, fmt.Sprint("offset=", offset))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										61
									
								
								internal/console/console.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								internal/console/console.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					package console
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type C[A any] struct {
 | 
				
			||||||
 | 
						io.Reader
 | 
				
			||||||
 | 
						io.Writer
 | 
				
			||||||
 | 
						err       io.Writer
 | 
				
			||||||
 | 
						args      A
 | 
				
			||||||
 | 
						abort     func()
 | 
				
			||||||
 | 
						cancelfns []func(context.Context) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New[A any](args A) (context.Context, *C[A]) {
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						ctx, abort := context.WithCancel(ctx)
 | 
				
			||||||
 | 
						ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
 | 
				
			||||||
 | 
						go func() { <-ctx.Done(); stop() }() // restore interrupt function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console := &C[A]{Reader: os.Stdin, Writer: os.Stdout, err: os.Stderr, args: args, abort: abort}
 | 
				
			||||||
 | 
						return ctx, console
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *C[A]) Args() A {
 | 
				
			||||||
 | 
						return c.args
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *C[A]) Shutdown() error {
 | 
				
			||||||
 | 
						fmt.Fprintln(c.err, "shutting down ", len(c.cancelfns), " cancel functions...")
 | 
				
			||||||
 | 
						defer fmt.Fprintln(c.err, "done")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.abort()
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						for _, fn := range c.cancelfns {
 | 
				
			||||||
 | 
							err = errors.Join(err, fn(ctx))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *C[A]) AddCancel(fn func(context.Context) error) { c.cancelfns = append(c.cancelfns, fn) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *C[A]) IfFatal(err error) {
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Fprintln(c.err, err)
 | 
				
			||||||
 | 
						err = c.Shutdown()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Fprintln(c.err, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						os.Exit(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								internal/env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/env/env.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Default(key, def string) string {
 | 
				
			||||||
 | 
						if v, ok := os.LookupEnv(key); ok {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return def
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type secret struct {
 | 
				
			||||||
 | 
						value string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s secret) Secret() string {
 | 
				
			||||||
 | 
						return s.value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s secret) String() string {
 | 
				
			||||||
 | 
						return "***"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Secret(key, def string) secret {
 | 
				
			||||||
 | 
						if v, ok := os.LookupEnv(key); ok {
 | 
				
			||||||
 | 
							return secret{v}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return secret{def}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DotEnv() {
 | 
				
			||||||
 | 
						fd, err := os.Open(".env")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scan := bufio.NewScanner(fd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for scan.Scan() {
 | 
				
			||||||
 | 
							line := scan.Text()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if strings.HasPrefix(line, "#") {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							key, val, ok := strings.Cut(line, "=")
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							os.Setenv(strings.TrimSpace(key), strings.TrimSpace(val))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										141
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								main.go
									
									
									
									
									
								
							@ -1,111 +1,16 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bufio"
 | 
					 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/signal"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"go.opentelemetry.io/otel/metric"
 | 
						"go.sour.is/xt/internal/console"
 | 
				
			||||||
 | 
						"go.sour.is/xt/internal/env"
 | 
				
			||||||
	"go.sour.is/xt/internal/otel"
 | 
						"go.sour.is/xt/internal/otel"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const name = "go.sour.is/xt"
 | 
					const name = "go.sour.is/xt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var m_up metric.Int64Gauge
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					 | 
				
			||||||
	dotEnv() // load .env
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ctx, console := newConsole(args{
 | 
					 | 
				
			||||||
		dbtype:   env("XT_DBTYPE", "sqlite3"),
 | 
					 | 
				
			||||||
		dbfile:   env("XT_DBFILE", "file:twt.db"),
 | 
					 | 
				
			||||||
		baseFeed: env("XT_BASE_FEED", "feed"),
 | 
					 | 
				
			||||||
		Nick:     env("XT_NICK", "xuu"),
 | 
					 | 
				
			||||||
		URI:      env("XT_URI", "https://txt.sour.is/user/xuu/twtxt.txt"),
 | 
					 | 
				
			||||||
		Listen:   env("XT_LISTEN", ":8080"),
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	finish, err := otel.Init(ctx, name)
 | 
					 | 
				
			||||||
	console.IfFatal(err)
 | 
					 | 
				
			||||||
	console.AddCancel(finish)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	m_up, err = otel.Meter().Int64Gauge("up")
 | 
					 | 
				
			||||||
	console.IfFatal(err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	m_up.Record(ctx, 1)
 | 
					 | 
				
			||||||
	defer m_up.Record(context.Background(), 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = run(console)
 | 
					 | 
				
			||||||
	if !errors.Is(err, context.Canceled) {
 | 
					 | 
				
			||||||
		console.IfFatal(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type console struct {
 | 
					 | 
				
			||||||
	io.Reader
 | 
					 | 
				
			||||||
	io.Writer
 | 
					 | 
				
			||||||
	err io.Writer
 | 
					 | 
				
			||||||
	context.Context
 | 
					 | 
				
			||||||
	abort     func()
 | 
					 | 
				
			||||||
	cancelfns []func(context.Context) error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func newConsole(args args) (context.Context, *console) {
 | 
					 | 
				
			||||||
	ctx := context.Background()
 | 
					 | 
				
			||||||
	ctx, abort := context.WithCancel(ctx)
 | 
					 | 
				
			||||||
	ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
 | 
					 | 
				
			||||||
	go func() { <-ctx.Done(); stop() }() // restore interrupt function
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	console := &console{Reader: os.Stdin, Writer: os.Stdout, err: os.Stderr, Context: ctx, abort: abort}
 | 
					 | 
				
			||||||
	console.Set("console", console)
 | 
					 | 
				
			||||||
	console.Set("args", args)
 | 
					 | 
				
			||||||
	return ctx, console
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *console) Args() args {
 | 
					 | 
				
			||||||
	v, ok := c.Get("args").(args)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		return args{}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return v
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (c *console) Shutdown() error {
 | 
					 | 
				
			||||||
	fmt.Fprintln(c.err, "shutting down ", len(c.cancelfns), " cancel functions...")
 | 
					 | 
				
			||||||
	defer fmt.Fprintln(c.err, "done")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.abort()
 | 
					 | 
				
			||||||
	var err error
 | 
					 | 
				
			||||||
	for _, fn := range c.cancelfns {
 | 
					 | 
				
			||||||
		err = errors.Join(err, fn(c.Context))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
func (c *console) AddCancel(fn func(context.Context) error) { c.cancelfns = append(c.cancelfns, fn) }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *console) IfFatal(err error) {
 | 
					 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	fmt.Fprintln(c.err, err)
 | 
					 | 
				
			||||||
	c.abort()
 | 
					 | 
				
			||||||
	os.Exit(1)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type contextKey struct{ name string }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *console) Set(name string, value any) {
 | 
					 | 
				
			||||||
	c.Context = context.WithValue(c.Context, contextKey{name}, value)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *console) Get(name string) any {
 | 
					 | 
				
			||||||
	return c.Context.Value(contextKey{name})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type args struct {
 | 
					type args struct {
 | 
				
			||||||
	dbtype   string
 | 
						dbtype   string
 | 
				
			||||||
	dbfile   string
 | 
						dbfile   string
 | 
				
			||||||
@ -115,32 +20,30 @@ type args struct {
 | 
				
			|||||||
	Listen   string
 | 
						Listen   string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func env(key, def string) string {
 | 
					func main() {
 | 
				
			||||||
	if v, ok := os.LookupEnv(key); ok {
 | 
						env.DotEnv() // load .env
 | 
				
			||||||
		return v
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return def
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
func dotEnv() {
 | 
						ctx, console := console.New(args{
 | 
				
			||||||
	fd, err := os.Open(".env")
 | 
							dbtype:   env.Default("XT_DBTYPE", "sqlite3"),
 | 
				
			||||||
	if err != nil {
 | 
							dbfile:   env.Default("XT_DBFILE", "file:twt.db"),
 | 
				
			||||||
		return
 | 
							baseFeed: env.Default("XT_BASE_FEED", "feed"),
 | 
				
			||||||
	}
 | 
							Nick:     env.Default("XT_NICK", "xuu"),
 | 
				
			||||||
 | 
							URI:      env.Default("XT_URI", "https://txt.sour.is/user/xuu/twtxt.txt"),
 | 
				
			||||||
 | 
							Listen:   env.Default("XT_LISTEN", ":8080"),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	scan := bufio.NewScanner(fd)
 | 
						finish, err := otel.Init(ctx, name)
 | 
				
			||||||
 | 
						console.IfFatal(err)
 | 
				
			||||||
 | 
						console.AddCancel(finish)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for scan.Scan() {
 | 
						m_up, err := otel.Meter().Int64Gauge("up")
 | 
				
			||||||
		line := scan.Text()
 | 
						console.IfFatal(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if strings.HasPrefix(line, "#") {
 | 
						m_up.Record(ctx, 1)
 | 
				
			||||||
			continue
 | 
						defer m_up.Record(context.Background(), 0)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		key, val, ok := strings.Cut(line, "=")
 | 
					 | 
				
			||||||
		if !ok {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		os.Setenv(strings.TrimSpace(key), strings.TrimSpace(val))
 | 
						err = run(ctx, console)
 | 
				
			||||||
 | 
						if !errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
							console.IfFatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -26,8 +26,8 @@ const (
 | 
				
			|||||||
	TwoMinutes = 120
 | 
						TwoMinutes = 120
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func feedRefreshProcessor(c *console, app *appState) error {
 | 
					func feedRefreshProcessor(ctx context.Context, app *appState) error {
 | 
				
			||||||
	ctx, span := otel.Span(c.Context)
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
	defer span.End()
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sleeping_time, _ := otel.Meter().Int64Counter("xt_feed_sleep")
 | 
						sleeping_time, _ := otel.Meter().Int64Counter("xt_feed_sleep")
 | 
				
			||||||
@ -38,7 +38,7 @@ func feedRefreshProcessor(c *console, app *appState) error {
 | 
				
			|||||||
	fetch, close := NewFuncPool(ctx, 40, f.Fetch)
 | 
						fetch, close := NewFuncPool(ctx, 40, f.Fetch)
 | 
				
			||||||
	defer close()
 | 
						defer close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	db, err := app.DB(c)
 | 
						db, err := app.DB(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		span.RecordError(err)
 | 
							span.RecordError(err)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -69,7 +69,7 @@ func feedRefreshProcessor(c *console, app *appState) error {
 | 
				
			|||||||
			span.AddEvent("sleeping for ", trace.WithAttributes(attribute.Int("seconds", int(TwoMinutes))))
 | 
								span.AddEvent("sleeping for ", trace.WithAttributes(attribute.Int("seconds", int(TwoMinutes))))
 | 
				
			||||||
			select {
 | 
								select {
 | 
				
			||||||
			case <-time.After(TwoMinutes * time.Second):
 | 
								case <-time.After(TwoMinutes * time.Second):
 | 
				
			||||||
			case <-c.Done():
 | 
								case <-ctx.Done():
 | 
				
			||||||
				return nil
 | 
									return nil
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			span.End()
 | 
								span.End()
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user