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