chore: refactor http out
This commit is contained in:
		
							parent
							
								
									db93108d0b
								
							
						
					
					
						commit
						cd2c9abd1b
					
				
							
								
								
									
										305
									
								
								feed.go
									
									
									
									
									
								
							
							
						
						
									
										305
									
								
								feed.go
									
									
									
									
									
								
							@ -18,6 +18,8 @@ import (
 | 
				
			|||||||
	_ "embed"
 | 
						_ "embed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/oklog/ulid/v2"
 | 
						"github.com/oklog/ulid/v2"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/attribute"
 | 
				
			||||||
 | 
						"go.opentelemetry.io/otel/trace"
 | 
				
			||||||
	"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"
 | 
				
			||||||
@ -630,3 +632,306 @@ func refreshLastTwt(ctx context.Context, db db) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return err
 | 
						return err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchTwts(ctx context.Context, db db, uri string, limit int, offset int64) ([]types.Twt, int64, int64, error) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args := make([]any, 0, 3)
 | 
				
			||||||
 | 
						where := `where feed_id in (select feed_id from feeds where state != 'permanantly-dead')`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if uri != "" {
 | 
				
			||||||
 | 
							feed_id := urlNS.UUID5(uri)
 | 
				
			||||||
 | 
							where = "where feed_id = ?"
 | 
				
			||||||
 | 
							args = append(args, feed_id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var end int64
 | 
				
			||||||
 | 
						err := db.QueryRowContext(ctx, `
 | 
				
			||||||
 | 
							select count(*) n from twts `+where+``, args...).Scan(&end)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if offset < 1 {
 | 
				
			||||||
 | 
							offset += end
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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),
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qry := `
 | 
				
			||||||
 | 
							SELECT
 | 
				
			||||||
 | 
								feed_id,
 | 
				
			||||||
 | 
								hash,
 | 
				
			||||||
 | 
								conv,
 | 
				
			||||||
 | 
								coalesce(nick, 'nobody') nick,
 | 
				
			||||||
 | 
								coalesce(uri, 'https://empty.txt') uri,
 | 
				
			||||||
 | 
								text
 | 
				
			||||||
 | 
							FROM twts
 | 
				
			||||||
 | 
							join (
 | 
				
			||||||
 | 
								select feed_id, nick, uri
 | 
				
			||||||
 | 
								from feeds
 | 
				
			||||||
 | 
							) using (feed_id)
 | 
				
			||||||
 | 
							where rowid in (
 | 
				
			||||||
 | 
								select rowid from twts
 | 
				
			||||||
 | 
								` + where + `
 | 
				
			||||||
 | 
								order by ulid asc
 | 
				
			||||||
 | 
								limit ?
 | 
				
			||||||
 | 
								offset ?
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							order by ulid asc`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Println(qry, args)
 | 
				
			||||||
 | 
						rows, err := db.QueryContext(
 | 
				
			||||||
 | 
							ctx, qry, args...,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							span.RecordError(err)
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var twts []types.Twt
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var o struct {
 | 
				
			||||||
 | 
								FeedID string
 | 
				
			||||||
 | 
								Hash   string
 | 
				
			||||||
 | 
								Conv   string
 | 
				
			||||||
 | 
								Dt     string
 | 
				
			||||||
 | 
								Nick   string
 | 
				
			||||||
 | 
								URI    string
 | 
				
			||||||
 | 
								Text   string
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								span.RecordError(err)
 | 
				
			||||||
 | 
								return nil, 0, 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							twter := types.NewTwter(o.Nick, o.URI)
 | 
				
			||||||
 | 
							twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
				
			||||||
 | 
							twts = append(twts, twt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return twts, offset, end, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchUsers(ctx context.Context, db db, uri, q string) ([]types.Twt, error) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						where := `where parent_id is null and state not in ('permanantly-dead', 'frozen') and last_twt_on is not null`
 | 
				
			||||||
 | 
						args := make([]any, 0)
 | 
				
			||||||
 | 
						if uri != "" {
 | 
				
			||||||
 | 
							where = `where feed_id = ? or parent_id = ?`
 | 
				
			||||||
 | 
							feed_id := urlNS.UUID5(uri)
 | 
				
			||||||
 | 
							args = append(args, feed_id, feed_id)
 | 
				
			||||||
 | 
						} else if q != "" {
 | 
				
			||||||
 | 
							where = `where nick like ?`
 | 
				
			||||||
 | 
							args = append(args, "%"+q+"%")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qry := `
 | 
				
			||||||
 | 
						SELECT
 | 
				
			||||||
 | 
							feed_id,
 | 
				
			||||||
 | 
							uri,
 | 
				
			||||||
 | 
							nick,
 | 
				
			||||||
 | 
							last_scan_on,
 | 
				
			||||||
 | 
							coalesce(last_twt_on, last_scan_on) last_twt_on
 | 
				
			||||||
 | 
						FROM feeds
 | 
				
			||||||
 | 
						left join last_twt_on using (feed_id)
 | 
				
			||||||
 | 
						` + where + `
 | 
				
			||||||
 | 
						order by nick, uri
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						fmt.Println(qry, args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := db.QueryContext(ctx, qry, args...)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							span.RecordError(err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var twts []types.Twt
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var o struct {
 | 
				
			||||||
 | 
								FeedID    string
 | 
				
			||||||
 | 
								URI       string
 | 
				
			||||||
 | 
								Nick      string
 | 
				
			||||||
 | 
								Dt        TwtTime
 | 
				
			||||||
 | 
								LastTwtOn TwtTime
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = rows.Scan(&o.FeedID, &o.URI, &o.Nick, &o.Dt, &o.LastTwtOn)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								span.RecordError(err)
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							twts = append(twts, lextwt.NewTwt(
 | 
				
			||||||
 | 
								types.NewTwter(o.Nick, o.URI),
 | 
				
			||||||
 | 
								lextwt.NewDateTime(o.Dt.Time, o.LastTwtOn.Time.Format(time.RFC3339)),
 | 
				
			||||||
 | 
								nil,
 | 
				
			||||||
 | 
							))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return twts, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchMentions(ctx context.Context, db db, mention uuid, limit int, offset int64) ([]types.Twt, int64, int64, error) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args := make([]any, 0, 3)
 | 
				
			||||||
 | 
						args = append(args, mention)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var end int64
 | 
				
			||||||
 | 
						err := db.QueryRowContext(ctx, `
 | 
				
			||||||
 | 
							select count(*) n from twt_mentions where feed_id = ?`, args...).Scan(&end)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Println(mention.MarshalText(), end, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if offset < 1 {
 | 
				
			||||||
 | 
							offset += end
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit = min(100, max(1, limit))
 | 
				
			||||||
 | 
						offset = max(1, offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args = append(args, limit, offset-int64(limit))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qry := `
 | 
				
			||||||
 | 
						SELECT
 | 
				
			||||||
 | 
							feed_id,
 | 
				
			||||||
 | 
							hash,
 | 
				
			||||||
 | 
							conv,
 | 
				
			||||||
 | 
							coalesce(nick, 'nobody') nick,
 | 
				
			||||||
 | 
							coalesce(uri, 'https://empty.txt') uri,
 | 
				
			||||||
 | 
							text
 | 
				
			||||||
 | 
						FROM twts
 | 
				
			||||||
 | 
						join (
 | 
				
			||||||
 | 
							select feed_id, nick, uri
 | 
				
			||||||
 | 
							from feeds
 | 
				
			||||||
 | 
						) using (feed_id)
 | 
				
			||||||
 | 
						where rowid in (
 | 
				
			||||||
 | 
							select rowid from twts
 | 
				
			||||||
 | 
							where ulid in (select ulid from twt_mentions where feed_id = ?)
 | 
				
			||||||
 | 
							order by ulid asc
 | 
				
			||||||
 | 
							limit ?
 | 
				
			||||||
 | 
							offset ?
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						order by ulid asc
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						fmt.Println(qry, args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := db.QueryContext(
 | 
				
			||||||
 | 
							ctx, qry, args...,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							span.RecordError(err)
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var twts []types.Twt
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var o struct {
 | 
				
			||||||
 | 
								FeedID string
 | 
				
			||||||
 | 
								Hash   string
 | 
				
			||||||
 | 
								Conv   string
 | 
				
			||||||
 | 
								Dt     string
 | 
				
			||||||
 | 
								Nick   string
 | 
				
			||||||
 | 
								URI    string
 | 
				
			||||||
 | 
								Text   string
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								span.RecordError(err)
 | 
				
			||||||
 | 
								return nil, 0, 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							twter := types.NewTwter(o.Nick, o.URI)
 | 
				
			||||||
 | 
							// o.Text = strings.ReplaceAll(o.Text, "\n", "\u2028")
 | 
				
			||||||
 | 
							twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
				
			||||||
 | 
							twts = append(twts, twt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := rows.Err(); err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err)
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return twts, offset, end, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func fetchConv(ctx context.Context, db db, hash string, limit int, offset int64) ([]types.Twt, int64, int64, error) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var end int64
 | 
				
			||||||
 | 
						err := db.QueryRowContext(ctx, `
 | 
				
			||||||
 | 
							select count(*) n from twts where hash = $1 or conv = $1`, hash).Scan(&end)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rows, err := db.QueryContext(
 | 
				
			||||||
 | 
							ctx, `
 | 
				
			||||||
 | 
							SELECT
 | 
				
			||||||
 | 
								feed_id,
 | 
				
			||||||
 | 
								hash,
 | 
				
			||||||
 | 
								conv,
 | 
				
			||||||
 | 
								nick,
 | 
				
			||||||
 | 
								uri,
 | 
				
			||||||
 | 
								text
 | 
				
			||||||
 | 
							FROM twts
 | 
				
			||||||
 | 
							JOIN (
 | 
				
			||||||
 | 
								SELECT
 | 
				
			||||||
 | 
									feed_id,
 | 
				
			||||||
 | 
									nick,
 | 
				
			||||||
 | 
									uri
 | 
				
			||||||
 | 
								FROM feeds
 | 
				
			||||||
 | 
							) using (feed_id)
 | 
				
			||||||
 | 
							WHERE
 | 
				
			||||||
 | 
								hash = $1 or
 | 
				
			||||||
 | 
								conv = $1
 | 
				
			||||||
 | 
							order by ulid asc`, hash,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							span.RecordError(err)
 | 
				
			||||||
 | 
							return nil, 0, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var twts []types.Twt
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var o struct {
 | 
				
			||||||
 | 
								FeedID string
 | 
				
			||||||
 | 
								Hash   string
 | 
				
			||||||
 | 
								Conv   string
 | 
				
			||||||
 | 
								Dt     string
 | 
				
			||||||
 | 
								Nick   string
 | 
				
			||||||
 | 
								URI    string
 | 
				
			||||||
 | 
								Text   string
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								span.RecordError(err)
 | 
				
			||||||
 | 
								return nil, 0,0,err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							twter := types.NewTwter(o.Nick, o.URI)
 | 
				
			||||||
 | 
							twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
				
			||||||
 | 
							twts = append(twts, twt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = rows.Err()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return twts, offset, end, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										554
									
								
								http.go
									
									
									
									
									
								
							
							
						
						
									
										554
									
								
								http.go
									
									
									
									
									
								
							@ -11,24 +11,20 @@ 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.sour.is/xt/internal/otel"
 | 
						"go.sour.is/xt/internal/otel"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const iAmTheWatcher = "I am the Watcher. I am your guide through this vast new twtiverse."
 | 
					const iAmTheWatcher = "I am the Watcher. I am your guide through this vast new twtiverse."
 | 
				
			||||||
const hostname = "https://watcher.sour.is"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var PREAMBLE_DOCS = func() lextwt.Comments {
 | 
					var mkPreambleDocs = func(hostname string) lextwt.Comments {
 | 
				
			||||||
	c := add(nil, iAmTheWatcher)
 | 
						c := add(nil, iAmTheWatcher)
 | 
				
			||||||
	c = add(c, "")
 | 
						c = add(c, "")
 | 
				
			||||||
	c = add(c, "Usage:")
 | 
						c = add(c, "Usage:")
 | 
				
			||||||
	c = add(c, "    %s/api/plain/users              View list of users and latest twt date.", hostname)
 | 
						c = add(c, "    %s/api/plain/users              View list of users and latest twt date.", hostname)
 | 
				
			||||||
	c = add(c, "    %s/api/plain/twt                View all twts in ascending order.", hostname)
 | 
						c = add(c, "    %s/api/plain/twt                View all twts.", hostname)
 | 
				
			||||||
	c = add(c, "    %s/api/plain/mentions?uri=:uri  View all mentions for uri in ascending order.", hostname)
 | 
						c = add(c, "    %s/api/plain/mentions?uri=:uri  View all mentions for uri.", hostname)
 | 
				
			||||||
	c = add(c, "    %s/api/plain/conv/:hash         View all twts for a conversation subject.", hostname)
 | 
						c = add(c, "    %s/api/plain/conv/:hash         View all twts for a conversation subject.", hostname)
 | 
				
			||||||
	c = add(c, "")
 | 
						c = add(c, "")
 | 
				
			||||||
	c = add(c, "Options:")
 | 
						c = add(c, "Options:")
 | 
				
			||||||
@ -36,7 +32,7 @@ var PREAMBLE_DOCS = func() lextwt.Comments {
 | 
				
			|||||||
	c = add(c, "    offset  Start index for quey.")
 | 
						c = add(c, "    offset  Start index for quey.")
 | 
				
			||||||
	c = add(c, "    limit   Count of items to return (going back in time).")
 | 
						c = add(c, "    limit   Count of items to return (going back in time).")
 | 
				
			||||||
	return add(c, "")
 | 
						return add(c, "")
 | 
				
			||||||
}()
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func httpServer(ctx context.Context, app *appState) error {
 | 
					func httpServer(ctx context.Context, app *appState) error {
 | 
				
			||||||
	ctx, span := otel.Span(ctx)
 | 
						ctx, span := otel.Span(ctx)
 | 
				
			||||||
@ -50,6 +46,12 @@ func httpServer(ctx context.Context, app *appState) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						api := API{
 | 
				
			||||||
 | 
							app:      app,
 | 
				
			||||||
 | 
							db:       db,
 | 
				
			||||||
 | 
							hostname: app.args.Hostname,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
						http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		_, span := otel.Span(r.Context())
 | 
							_, span := otel.Span(r.Context())
 | 
				
			||||||
		defer span.End()
 | 
							defer span.End()
 | 
				
			||||||
@ -58,379 +60,13 @@ func httpServer(ctx context.Context, app *appState) error {
 | 
				
			|||||||
		w.Write([]byte("ok"))
 | 
							w.Write([]byte("ok"))
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	http.HandleFunc("/api/plain", func(w http.ResponseWriter, r *http.Request) {
 | 
						http.HandleFunc("/api/plain", api.plain)
 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(PREAMBLE_DOCS, nil)
 | 
						http.HandleFunc("/api/plain/conv/{hash}", api.conv)
 | 
				
			||||||
		reg.WriteTo(w)
 | 
						http.HandleFunc("/api/plain/mentions", api.mentions)
 | 
				
			||||||
	})
 | 
						http.HandleFunc("/api/plain/twt", api.twt)
 | 
				
			||||||
 | 
						http.HandleFunc("/api/plain/tweets", api.twt)
 | 
				
			||||||
	http.HandleFunc("/api/plain/conv/{hash}", func(w http.ResponseWriter, r *http.Request) {
 | 
						http.HandleFunc("/api/plain/users", api.users)
 | 
				
			||||||
		ctx, span := otel.Span(r.Context())
 | 
						http.HandleFunc("/api/plain/queue", api.queue)
 | 
				
			||||||
		defer span.End()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		hash := r.PathValue("hash")
 | 
					 | 
				
			||||||
		if (len(hash) < 6 || len(hash) > 8) && !notAny(hash, "abcdefghijklmnopqrstuvwxyz234567") {
 | 
					 | 
				
			||||||
			w.WriteHeader(http.StatusBadRequest)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		rows, err := db.QueryContext(
 | 
					 | 
				
			||||||
			ctx, `
 | 
					 | 
				
			||||||
				SELECT
 | 
					 | 
				
			||||||
					feed_id,
 | 
					 | 
				
			||||||
					hash,
 | 
					 | 
				
			||||||
					conv,
 | 
					 | 
				
			||||||
					nick,
 | 
					 | 
				
			||||||
					uri,
 | 
					 | 
				
			||||||
					text
 | 
					 | 
				
			||||||
				FROM twts
 | 
					 | 
				
			||||||
				JOIN (
 | 
					 | 
				
			||||||
					SELECT
 | 
					 | 
				
			||||||
						feed_id,
 | 
					 | 
				
			||||||
						nick,
 | 
					 | 
				
			||||||
						uri
 | 
					 | 
				
			||||||
					FROM feeds
 | 
					 | 
				
			||||||
				) using (feed_id)
 | 
					 | 
				
			||||||
				WHERE
 | 
					 | 
				
			||||||
					hash = $1 or
 | 
					 | 
				
			||||||
					conv = $1
 | 
					 | 
				
			||||||
				order by ulid asc`, hash,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			span.RecordError(err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		defer rows.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var twts []types.Twt
 | 
					 | 
				
			||||||
		for rows.Next() {
 | 
					 | 
				
			||||||
			var o struct {
 | 
					 | 
				
			||||||
				FeedID string
 | 
					 | 
				
			||||||
				Hash   string
 | 
					 | 
				
			||||||
				Conv   string
 | 
					 | 
				
			||||||
				Dt     string
 | 
					 | 
				
			||||||
				Nick   string
 | 
					 | 
				
			||||||
				URI    string
 | 
					 | 
				
			||||||
				Text   string
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				span.RecordError(err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			twter := types.NewTwter(o.Nick, o.URI)
 | 
					 | 
				
			||||||
			o.Text = strings.ReplaceAll(o.Text, "\n", "\u2028")
 | 
					 | 
				
			||||||
			twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
					 | 
				
			||||||
			twts = append(twts, twt)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		preamble := add(PREAMBLE_DOCS, "self = /conv/%s", hash)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
					 | 
				
			||||||
		reg.WriteTo(w)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/api/plain/mentions", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		ctx, span := otel.Span(r.Context())
 | 
					 | 
				
			||||||
		defer span.End()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		uri := r.URL.Query().Get("uri")
 | 
					 | 
				
			||||||
		if uri == "" {
 | 
					 | 
				
			||||||
			reg := lextwt.NewTwtRegistry(PREAMBLE_DOCS, nil)
 | 
					 | 
				
			||||||
			reg.WriteTo(w)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		mention := urlNS.UUID5(uri)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		args := make([]any, 0, 3)
 | 
					 | 
				
			||||||
		args = append(args, mention)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		limit := 100
 | 
					 | 
				
			||||||
		if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
 | 
					 | 
				
			||||||
			limit = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var offset int64 = 0
 | 
					 | 
				
			||||||
		if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
					 | 
				
			||||||
			offset = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var end int64
 | 
					 | 
				
			||||||
		err = db.QueryRowContext(ctx, `
 | 
					 | 
				
			||||||
			select count(*) n from twt_mentions where feed_id = ?`, args...).Scan(&end)
 | 
					 | 
				
			||||||
		span.RecordError(err)
 | 
					 | 
				
			||||||
		fmt.Println(mention.MarshalText(), end, err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if offset < 1 {
 | 
					 | 
				
			||||||
			offset += end
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		limit = min(100, max(1, limit))
 | 
					 | 
				
			||||||
		offset = max(1, offset)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		args = append(args, limit, offset-int64(limit))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		qry := `
 | 
					 | 
				
			||||||
			SELECT
 | 
					 | 
				
			||||||
				feed_id,
 | 
					 | 
				
			||||||
				hash,
 | 
					 | 
				
			||||||
				conv,
 | 
					 | 
				
			||||||
				coalesce(nick, 'nobody') nick,
 | 
					 | 
				
			||||||
				coalesce(uri, 'https://empty.txt') uri,
 | 
					 | 
				
			||||||
				text
 | 
					 | 
				
			||||||
			FROM twts
 | 
					 | 
				
			||||||
			join (
 | 
					 | 
				
			||||||
				select feed_id, nick, uri
 | 
					 | 
				
			||||||
				from feeds
 | 
					 | 
				
			||||||
			) using (feed_id)
 | 
					 | 
				
			||||||
			where rowid in (
 | 
					 | 
				
			||||||
				select rowid from twts
 | 
					 | 
				
			||||||
				where ulid in (select ulid from twt_mentions where feed_id = ?)
 | 
					 | 
				
			||||||
				order by ulid asc
 | 
					 | 
				
			||||||
				limit ?
 | 
					 | 
				
			||||||
				offset ?
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			order by ulid asc
 | 
					 | 
				
			||||||
		`
 | 
					 | 
				
			||||||
		fmt.Println(qry, args)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		rows, err := db.QueryContext(
 | 
					 | 
				
			||||||
			ctx, qry, args...,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			span.RecordError(err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		defer rows.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var twts []types.Twt
 | 
					 | 
				
			||||||
		for rows.Next() {
 | 
					 | 
				
			||||||
			var o struct {
 | 
					 | 
				
			||||||
				FeedID string
 | 
					 | 
				
			||||||
				Hash   string
 | 
					 | 
				
			||||||
				Conv   string
 | 
					 | 
				
			||||||
				Dt     string
 | 
					 | 
				
			||||||
				Nick   string
 | 
					 | 
				
			||||||
				URI    string
 | 
					 | 
				
			||||||
				Text   string
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				span.RecordError(err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			twter := types.NewTwter(o.Nick, o.URI)
 | 
					 | 
				
			||||||
			o.Text = strings.ReplaceAll(o.Text, "\n", "\u2028")
 | 
					 | 
				
			||||||
			twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
					 | 
				
			||||||
			twts = append(twts, twt)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if err := rows.Err(); err != nil {
 | 
					 | 
				
			||||||
			fmt.Println(err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		preamble := add(PREAMBLE_DOCS, "twt range = 1  %d", end)
 | 
					 | 
				
			||||||
		preamble = add(preamble, "self = %s/mentions%s", hostname, mkqry(uri, limit, offset))
 | 
					 | 
				
			||||||
		if next := offset + int64(len(twts)); next < end {
 | 
					 | 
				
			||||||
			preamble = add(preamble, "next = %s/mentions%s", hostname, mkqry(uri, limit, next))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if prev := offset - int64(limit); prev > 0 {
 | 
					 | 
				
			||||||
			preamble = add(preamble, "prev = %s/mentions%s", hostname, mkqry(uri, limit, prev))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
					 | 
				
			||||||
		reg.WriteTo(w)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/api/plain/twt", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		ctx, span := otel.Span(r.Context())
 | 
					 | 
				
			||||||
		defer span.End()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		uri := r.URL.Query().Get("uri")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		limit := 100
 | 
					 | 
				
			||||||
		if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
 | 
					 | 
				
			||||||
			limit = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		limit = min(100, max(1, limit))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var offset int64 = 0
 | 
					 | 
				
			||||||
		if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
					 | 
				
			||||||
			offset = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		twts, offset, end, err := func(uri string, limit int, offset int64) ([]types.Twt, int64, int64, error) {
 | 
					 | 
				
			||||||
			args := make([]any, 0, 3)
 | 
					 | 
				
			||||||
			where := `where feed_id in (select feed_id from feeds where state != 'permanantly-dead')`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if uri != "" {
 | 
					 | 
				
			||||||
				feed_id := urlNS.UUID5(uri)
 | 
					 | 
				
			||||||
				where = "where feed_id = ?"
 | 
					 | 
				
			||||||
				args = append(args, feed_id)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var end int64
 | 
					 | 
				
			||||||
			err = db.QueryRowContext(ctx, `
 | 
					 | 
				
			||||||
				select count(*) n from twts `+where+``, args...).Scan(&end)
 | 
					 | 
				
			||||||
			span.RecordError(err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if offset < 1 {
 | 
					 | 
				
			||||||
				offset += end
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			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),
 | 
					 | 
				
			||||||
			))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			qry := `
 | 
					 | 
				
			||||||
				SELECT
 | 
					 | 
				
			||||||
					feed_id,
 | 
					 | 
				
			||||||
					hash,
 | 
					 | 
				
			||||||
					conv,
 | 
					 | 
				
			||||||
					coalesce(nick, 'nobody') nick,
 | 
					 | 
				
			||||||
					coalesce(uri, 'https://empty.txt') uri,
 | 
					 | 
				
			||||||
					text
 | 
					 | 
				
			||||||
				FROM twts
 | 
					 | 
				
			||||||
				join (
 | 
					 | 
				
			||||||
					select feed_id, nick, uri
 | 
					 | 
				
			||||||
					from feeds
 | 
					 | 
				
			||||||
				) using (feed_id)
 | 
					 | 
				
			||||||
				where rowid in (
 | 
					 | 
				
			||||||
					select rowid from twts
 | 
					 | 
				
			||||||
					` + where + `
 | 
					 | 
				
			||||||
					order by ulid asc
 | 
					 | 
				
			||||||
					limit ?
 | 
					 | 
				
			||||||
					offset ?
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				order by ulid asc`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			fmt.Println(qry, args)
 | 
					 | 
				
			||||||
			w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
					 | 
				
			||||||
			rows, err := db.QueryContext(
 | 
					 | 
				
			||||||
				ctx, qry, args...,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				span.RecordError(err)
 | 
					 | 
				
			||||||
				return nil, 0, 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			defer rows.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var twts []types.Twt
 | 
					 | 
				
			||||||
			for rows.Next() {
 | 
					 | 
				
			||||||
				var o struct {
 | 
					 | 
				
			||||||
					FeedID string
 | 
					 | 
				
			||||||
					Hash   string
 | 
					 | 
				
			||||||
					Conv   string
 | 
					 | 
				
			||||||
					Dt     string
 | 
					 | 
				
			||||||
					Nick   string
 | 
					 | 
				
			||||||
					URI    string
 | 
					 | 
				
			||||||
					Text   string
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				err = rows.Scan(&o.FeedID, &o.Hash, &o.Conv, &o.Nick, &o.URI, &o.Text)
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					span.RecordError(err)
 | 
					 | 
				
			||||||
					return nil, 0, 0, err
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				twter := types.NewTwter(o.Nick, o.URI)
 | 
					 | 
				
			||||||
				twt, _ := lextwt.ParseLine(o.Text, &twter)
 | 
					 | 
				
			||||||
				twts = append(twts, twt)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return twts, offset, end, err
 | 
					 | 
				
			||||||
		} (uri, limit, offset)
 | 
					 | 
				
			||||||
		span.RecordError(err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		preamble := add(PREAMBLE_DOCS, "twt range = 1  %d", end)
 | 
					 | 
				
			||||||
		preamble = add(preamble, "self = %s/api/plain/twt%s", hostname, mkqry(uri, limit, offset))
 | 
					 | 
				
			||||||
		if next := offset + int64(len(twts)); next < end {
 | 
					 | 
				
			||||||
			preamble = add(preamble, "next = %s/api/plain/twt%s", hostname, mkqry(uri, limit, next))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if prev := offset - int64(limit); prev > 0 {
 | 
					 | 
				
			||||||
			preamble = add(preamble, "prev = %s/api/plain/twt%s", hostname, mkqry(uri, limit, prev))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
					 | 
				
			||||||
		reg.WriteTo(w)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/api/plain/users", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		ctx, span := otel.Span(r.Context())
 | 
					 | 
				
			||||||
		defer span.End()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		where := `where parent_id is null and state not in ('permanantly-dead', 'frozen') and last_twt_on is not null`
 | 
					 | 
				
			||||||
		args := make([]any, 0)
 | 
					 | 
				
			||||||
		if uri := r.URL.Query().Get("uri"); uri != "" {
 | 
					 | 
				
			||||||
			where = `where feed_id = ? or parent_id = ?`
 | 
					 | 
				
			||||||
			feed_id := urlNS.UUID5(uri)
 | 
					 | 
				
			||||||
			args = append(args, feed_id, feed_id)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		qry := `
 | 
					 | 
				
			||||||
			SELECT
 | 
					 | 
				
			||||||
				feed_id,
 | 
					 | 
				
			||||||
				uri,
 | 
					 | 
				
			||||||
				nick,
 | 
					 | 
				
			||||||
				last_scan_on,
 | 
					 | 
				
			||||||
				coalesce(last_twt_on, last_scan_on) last_twt_on
 | 
					 | 
				
			||||||
			FROM feeds
 | 
					 | 
				
			||||||
			left join last_twt_on using (feed_id)
 | 
					 | 
				
			||||||
			` + where + `
 | 
					 | 
				
			||||||
			order by nick, uri
 | 
					 | 
				
			||||||
		`
 | 
					 | 
				
			||||||
		fmt.Println(qry, args)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		rows, err := db.QueryContext(ctx, qry, args...)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			span.RecordError(err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		defer rows.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var twts []types.Twt
 | 
					 | 
				
			||||||
		for rows.Next() {
 | 
					 | 
				
			||||||
			var o struct {
 | 
					 | 
				
			||||||
				FeedID    string
 | 
					 | 
				
			||||||
				URI       string
 | 
					 | 
				
			||||||
				Nick      string
 | 
					 | 
				
			||||||
				Dt        TwtTime
 | 
					 | 
				
			||||||
				LastTwtOn TwtTime
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			err = rows.Scan(&o.FeedID, &o.URI, &o.Nick, &o.Dt, &o.LastTwtOn)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				span.RecordError(err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			twts = append(twts, lextwt.NewTwt(
 | 
					 | 
				
			||||||
				types.NewTwter(o.Nick, o.URI),
 | 
					 | 
				
			||||||
				lextwt.NewDateTime(o.Dt.Time, o.LastTwtOn.Time.Format(time.RFC3339)),
 | 
					 | 
				
			||||||
				nil,
 | 
					 | 
				
			||||||
			))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reg := lextwt.NewTwtRegistry(PREAMBLE_DOCS, twts)
 | 
					 | 
				
			||||||
		reg.WriteTo(w)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.HandleFunc("/api/plain/queue", func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
		lis := slices.Collect(app.queue.Iter())
 | 
					 | 
				
			||||||
		sort.Slice(lis, func(i, j int) bool {
 | 
					 | 
				
			||||||
			return lis[i].NextScanOn.Time.Before(lis[j].LastScanOn.Time)
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		for _, feed := range lis {
 | 
					 | 
				
			||||||
			fmt.Fprintln(w, feed.State, feed.NextScanOn.Time.Format(time.RFC3339), feed.Nick, feed.URI)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	srv := &http.Server{
 | 
						srv := &http.Server{
 | 
				
			||||||
		Addr:    app.args.Listen,
 | 
							Addr:    app.args.Listen,
 | 
				
			||||||
@ -484,3 +120,159 @@ func add(preamble lextwt.Comments, text string, v ...any) lextwt.Comments {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return append(preamble, lextwt.NewComment("# "+text))
 | 
						return append(preamble, lextwt.NewComment("# "+text))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mkPreamble(hostname, uri, path string, limit int, length, offset, end int64) lextwt.Comments {
 | 
				
			||||||
 | 
						uri += path
 | 
				
			||||||
 | 
						preamble := add(mkPreambleDocs(hostname), "twt range = 1  %d", end)
 | 
				
			||||||
 | 
						preamble = add(preamble, "self = %s%s", hostname, mkqry(uri, limit, offset))
 | 
				
			||||||
 | 
						if next := offset + length; next < end {
 | 
				
			||||||
 | 
							preamble = add(preamble, "next = %s%s", hostname, mkqry(uri, limit, next))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if prev := offset - int64(limit); prev > 0 {
 | 
				
			||||||
 | 
							preamble = add(preamble, "prev = %s%s", hostname, mkqry(uri, limit, prev))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return preamble
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type API struct {
 | 
				
			||||||
 | 
						app      *appState
 | 
				
			||||||
 | 
						db       db
 | 
				
			||||||
 | 
						hostname string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) plain(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						reg := lextwt.NewTwtRegistry(mkPreambleDocs(a.hostname), nil)
 | 
				
			||||||
 | 
						reg.WriteTo(w)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) conv(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(r.Context())
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						hash := r.PathValue("hash")
 | 
				
			||||||
 | 
						if (len(hash) < 6 || len(hash) > 8) && !notAny(hash, "abcdefghijklmnopqrstuvwxyz234567") {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit := 100
 | 
				
			||||||
 | 
						if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
 | 
				
			||||||
 | 
							limit = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var offset int64 = 0
 | 
				
			||||||
 | 
						if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
				
			||||||
 | 
							offset = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						twts, offset, end, err := fetchConv(ctx, a.db, hash, limit, offset)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "ERR", 500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						preamble := mkPreamble(a.hostname, "", "/api/plain/conv/"+hash, limit, int64(len(twts)), offset, end)
 | 
				
			||||||
 | 
						reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
				
			||||||
 | 
						reg.WriteTo(w)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) mentions(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(r.Context())
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uri := r.URL.Query().Get("uri")
 | 
				
			||||||
 | 
						if uri == "" {
 | 
				
			||||||
 | 
							reg := lextwt.NewTwtRegistry(mkPreambleDocs(a.hostname), nil)
 | 
				
			||||||
 | 
							reg.WriteTo(w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mention := urlNS.UUID5(uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit := 100
 | 
				
			||||||
 | 
						if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
 | 
				
			||||||
 | 
							limit = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var offset int64 = 0
 | 
				
			||||||
 | 
						if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
				
			||||||
 | 
							offset = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						twts, offset, end, err := fetchMentions(ctx, a.db, mention, limit, offset)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "ERR", 500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						preamble := mkPreamble(a.hostname, uri, "/api/plain/mentions", limit, int64(len(twts)), offset, end)
 | 
				
			||||||
 | 
						reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
				
			||||||
 | 
						reg.WriteTo(w)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) twt(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(r.Context())
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uri := r.URL.Query().Get("uri")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit := 100
 | 
				
			||||||
 | 
						if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
 | 
				
			||||||
 | 
							limit = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						limit = min(100, max(1, limit))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var offset int64 = 0
 | 
				
			||||||
 | 
						if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
 | 
				
			||||||
 | 
							offset = v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						twts, offset, end, err := fetchTwts(ctx, a.db, uri, limit, offset)
 | 
				
			||||||
 | 
						span.RecordError(err)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "ERR", 500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						preamble := mkPreamble(a.hostname, uri, "/api/plain/twt", limit, int64(len(twts)), offset, end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reg := lextwt.NewTwtRegistry(preamble, twts)
 | 
				
			||||||
 | 
						reg.WriteTo(w)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) users(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						ctx, span := otel.Span(r.Context())
 | 
				
			||||||
 | 
						defer span.End()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uri := r.URL.Query().Get("uri")
 | 
				
			||||||
 | 
						q := r.URL.Query().Get("q")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						twts, err := fetchUsers(ctx, a.db, uri, q)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "ERR", 500)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reg := lextwt.NewTwtRegistry(mkPreambleDocs(a.hostname), twts)
 | 
				
			||||||
 | 
						reg.WriteTo(w)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *API) queue(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						lis := slices.Collect(a.app.queue.Iter())
 | 
				
			||||||
 | 
						sort.Slice(lis, func(i, j int) bool {
 | 
				
			||||||
 | 
							return lis[i].NextScanOn.Time.Before(lis[j].LastScanOn.Time)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						for _, feed := range lis {
 | 
				
			||||||
 | 
							fmt.Fprintln(w, feed.State, feed.NextScanOn.Time.Format(time.RFC3339), feed.Nick, feed.URI)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							@ -18,6 +18,7 @@ type args struct {
 | 
				
			|||||||
	Nick     string
 | 
						Nick     string
 | 
				
			||||||
	URI      string
 | 
						URI      string
 | 
				
			||||||
	Listen   string
 | 
						Listen   string
 | 
				
			||||||
 | 
						Hostname string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
@ -30,6 +31,7 @@ func main() {
 | 
				
			|||||||
		Nick:     env.Default("XT_NICK", "xuu"),
 | 
							Nick:     env.Default("XT_NICK", "xuu"),
 | 
				
			||||||
		URI:      env.Default("XT_URI", "https://txt.sour.is/user/xuu/twtxt.txt"),
 | 
							URI:      env.Default("XT_URI", "https://txt.sour.is/user/xuu/twtxt.txt"),
 | 
				
			||||||
		Listen:   env.Default("XT_LISTEN", ":8080"),
 | 
							Listen:   env.Default("XT_LISTEN", ":8080"),
 | 
				
			||||||
 | 
							Hostname: env.Default("XT_HOSTNAME", "https://watcher.sour.is"),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	finish, err := otel.Init(ctx, name)
 | 
						finish, err := otel.Init(ctx, name)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user