306 lines
6.6 KiB
Go
306 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.yarn.social/lextwt"
|
|
"go.yarn.social/types"
|
|
|
|
"go.sour.is/xt/internal/otel"
|
|
)
|
|
|
|
func httpServer(c *console, app *appState) error {
|
|
ctx, span := otel.Span(c)
|
|
defer span.End()
|
|
|
|
span.AddEvent("start http server")
|
|
|
|
db, err := app.DB(ctx)
|
|
if err != nil {
|
|
span.RecordError(fmt.Errorf("%w: missing db", err))
|
|
return err
|
|
}
|
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, span := otel.Span(r.Context())
|
|
defer span.End()
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Write([]byte("ok"))
|
|
})
|
|
|
|
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)
|
|
}
|
|
var preamble lextwt.Comments
|
|
preamble = append(preamble, lextwt.NewComment("# self = /conv/"+hash))
|
|
|
|
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()
|
|
|
|
args := make([]any, 0, 3)
|
|
uriarg := ""
|
|
uri := r.URL.Query().Get("uri")
|
|
if uri != "" {
|
|
uriarg = "and uri = ?"
|
|
args = append(args, uri)
|
|
}
|
|
|
|
limit := 100
|
|
if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
|
|
limit = v
|
|
}
|
|
|
|
offset := 0
|
|
if v, ok := strconv.Atoi(r.URL.Query().Get("offset")); ok == nil {
|
|
offset = v
|
|
}
|
|
args = append(args, limit, offset)
|
|
|
|
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
|
|
where state not in ('frozen', 'permanantly-dead')
|
|
`+uriarg+`
|
|
) using (feed_id)
|
|
order by ulid desc
|
|
limit ?
|
|
offset ?
|
|
`, 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)
|
|
}
|
|
var preamble lextwt.Comments
|
|
preamble = append(preamble, lextwt.NewComment("# I am the Watcher. I am your guide through this vast new twtiverse."))
|
|
preamble = append(preamble, lextwt.NewComment("# self = /api/plain/twts"+mkqry(uri, limit, offset)))
|
|
preamble = append(preamble, lextwt.NewComment("# next = /api/plain/twts"+mkqry(uri, limit, offset+len(twts))))
|
|
if offset > 0 {
|
|
preamble = append(preamble, lextwt.NewComment("# prev = /api/plain/twts"+mkqry(uri, limit, offset-limit)))
|
|
}
|
|
|
|
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")
|
|
|
|
rows, err := db.QueryContext(
|
|
ctx,
|
|
`SELECT
|
|
feed_id,
|
|
uri,
|
|
nick,
|
|
last_scan_on,
|
|
last_twt_on
|
|
FROM feeds
|
|
left join (
|
|
select feed_id, max(strftime('%Y-%m-%dT%H:%M:%fZ', (substring(text, 1, instr(text, ' ')-1)))) last_twt_on
|
|
from twts group by feed_id
|
|
) using (feed_id)
|
|
where parent_id is null and state not in ('permanantly-dead', 'frozen') and last_twt_on is not null
|
|
order by nick, uri
|
|
`,
|
|
)
|
|
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(nil, 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{
|
|
Addr: app.args.Listen,
|
|
Handler: http.DefaultServeMux,
|
|
}
|
|
|
|
c.AddCancel(srv.Shutdown)
|
|
err = srv.ListenAndServe()
|
|
if !errors.Is(err, http.ErrServerClosed) {
|
|
span.RecordError(err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func notAny(s string, chars string) bool {
|
|
for _, c := range s {
|
|
if !strings.ContainsRune(chars, c) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
|
|
func mkqry(uri string, limit, offset int) string {
|
|
qry := make([]string, 0, 3)
|
|
|
|
if uri != "" {
|
|
qry = append(qry, "uri=" + uri)
|
|
}
|
|
|
|
limit = min(100, max(1, limit))
|
|
if limit != 100 {
|
|
qry = append(qry, fmt.Sprint("limit=", limit))
|
|
}
|
|
|
|
offset = max(0, offset)
|
|
if offset != 0 {
|
|
qry = append(qry, fmt.Sprint("offset=", offset))
|
|
}
|
|
|
|
if len(qry) == 0 {
|
|
return ""
|
|
}
|
|
|
|
return "?" + strings.Join(qry, "&")
|
|
}
|