diff --git a/feed.go b/feed.go index d5e0f3c..1720009 100644 --- a/feed.go +++ b/feed.go @@ -18,6 +18,8 @@ import ( _ "embed" "github.com/oklog/ulid/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "go.sour.is/xt/internal/otel" "go.yarn.social/lextwt" "go.yarn.social/types" @@ -630,3 +632,306 @@ func refreshLastTwt(ctx context.Context, db db) error { 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 +} \ No newline at end of file diff --git a/http.go b/http.go index 661187e..b5701df 100644 --- a/http.go +++ b/http.go @@ -11,24 +11,20 @@ import ( "strings" "time" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "go.yarn.social/lextwt" - "go.yarn.social/types" "go.sour.is/xt/internal/otel" ) 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(c, "") 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/twt View all twts in ascending order.", 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/twt View all twts.", 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, "") 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, " limit Count of items to return (going back in time).") return add(c, "") -}() +} func httpServer(ctx context.Context, app *appState) error { ctx, span := otel.Span(ctx) @@ -50,6 +46,12 @@ func httpServer(ctx context.Context, app *appState) error { return err } + api := API{ + app: app, + db: db, + hostname: app.args.Hostname, + } + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, span := otel.Span(r.Context()) defer span.End() @@ -58,379 +60,13 @@ func httpServer(ctx context.Context, app *appState) error { w.Write([]byte("ok")) }) - http.HandleFunc("/api/plain", func(w http.ResponseWriter, r *http.Request) { - reg := lextwt.NewTwtRegistry(PREAMBLE_DOCS, nil) - reg.WriteTo(w) - }) - - http.HandleFunc("/api/plain/conv/{hash}", func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Span(r.Context()) - 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) - } - }) + http.HandleFunc("/api/plain", api.plain) + http.HandleFunc("/api/plain/conv/{hash}", api.conv) + 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/users", api.users) + http.HandleFunc("/api/plain/queue", api.queue) srv := &http.Server{ 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)) } + +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) + } +} diff --git a/main.go b/main.go index 506931a..875c294 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ type args struct { Nick string URI string Listen string + Hostname string } func main() { @@ -30,6 +31,7 @@ func main() { 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"), + Hostname: env.Default("XT_HOSTNAME", "https://watcher.sour.is"), }) finish, err := otel.Init(ctx, name)