package main import ( "fmt" "net/http" "slices" "sort" "strconv" "strings" "time" "go.sour.is/xt/internal/otel" "go.sour.is/xt/internal/uuid" "go.yarn.social/lextwt" ) 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 := uuid.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) } } 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 int, offset int64) string { qry := make([]string, 0, 3) if uri != "" { qry = append(qry, "uri="+uri) } if limit != 100 { qry = append(qry, fmt.Sprint("limit=", limit)) } if offset != 0 { qry = append(qry, fmt.Sprint("offset=", offset)) } if len(qry) == 0 { return "" } return "?" + strings.Join(qry, "&") } func add(preamble lextwt.Comments, text string, v ...any) lextwt.Comments { if len(v) > 0 { text = fmt.Sprintf(text, v...) } return append(preamble, lextwt.NewComment("# "+text)) } func addKey(preamble lextwt.Comments, key, value string, v ...any) lextwt.Comments { if len(v) > 0 { value = fmt.Sprintf(value, v...) } comment := fmt.Sprintf("# %s = %s", key, value) return append(preamble, lextwt.NewCommentValue(comment, key, value)) } func mkPreamble(hostname, uri, path string, limit int, length, offset, end int64) lextwt.Comments { preamble := addKey(mkPreambleDocs(hostname), "twt range", "1 %d", end) preamble = addKey(preamble, "self", "%s%s%s", hostname, path, mkqry(uri, limit, offset)) if next := offset + length; next < end { preamble = addKey(preamble, "next", "%s%s%s", hostname, path, mkqry(uri, limit, next)) } if prev := offset - int64(limit); prev > 0 { preamble = addKey(preamble, "prev", "%s%s%s", hostname, path, mkqry(uri, limit, prev)) } return preamble } 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.", 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:") c = add(c, " uri Filter to show a specific users twts.") c = add(c, " offset Start index for quey.") c = add(c, " limit Count of items to return (going back in time).") return add(c, "") }