Files
kjv_api/internal/kjv/sermons.go
T
2026-10-26 20:30:43 -07:00

164 lines
5.1 KiB
Go

package kjv
import (
"crypto/subtle"
"database/sql"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"time"
)
// Optional endpoints backed by the separate sermon database (KJV_SERMON_DB)
// and the separate semantic search service (KJV_SEMANTIC_ADDR, default
// localhost:1613). Neither is part of the distribution; without them these
// endpoints answer with an error.
func rowsToMaps(rows *sql.Rows) ([]map[string]any, error) {
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return nil, err
}
out := []map[string]any{}
for rows.Next() {
vals := make([]any, len(cols))
ptrs := make([]any, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err != nil {
return nil, err
}
m := map[string]any{}
for i, c := range cols {
if b, ok := vals[i].([]byte); ok {
vals[i] = string(b)
}
m[c] = vals[i]
}
out = append(out, m)
}
return out, rows.Err()
}
func (a *App) sermonQuery(query string, args ...any) Resp {
if a.sermonDB == nil {
return jsonStatus(map[string]string{"error": "Sermon database not found"}, http.StatusServiceUnavailable)
}
rows, err := a.sermonDB.Query(query, args...)
if err != nil {
lg.Error("sermon query: %v", err)
return errJSON(http.StatusInternalServerError, "Sermon database error")
}
out, err := rowsToMaps(rows)
if err != nil {
return errJSON(http.StatusInternalServerError, "Sermon database error")
}
return jsonR(out)
}
const sermonCols = "select distinct title,url,denomination,preacher,church_name from verse_refs where "
// handleSermons serves everything under /sermon/ and /sermons/ ... by hand,
// since the paths overlap in ways the router would reject.
func (a *App) handleSermons(path string) Resp {
parts := strings.Split(strings.Trim(path, "/"), "/")
switch {
case len(parts) == 1 && parts[0] == "sermons":
return a.sermonQuery("select distinct title,url,denomination,preacher,church_name,filename from verse_refs")
case len(parts) == 3 && parts[0] == "sermons" && parts[1] == "preacher":
return a.sermonQuery(sermonCols+"preacher=?", parts[2])
case len(parts) == 3 && parts[0] == "sermons" && parts[1] == "church":
return a.sermonQuery(sermonCols+"church_name=?", parts[2])
case len(parts) == 3 && parts[0] == "sermons" && parts[1] == "denomination":
return a.sermonQuery(sermonCols+"denomination=?", parts[2])
case len(parts) == 3 && parts[0] == "sermon" && parts[1] == "search":
return a.sermonsByRef(parts[2])
case len(parts) == 3 && parts[0] == "sermon" && parts[1] == "url":
return a.sermonQuery(sermonCols+"url=?", parts[2])
case len(parts) == 3 && parts[0] == "sermon" && parts[2] == "verses":
return a.sermonQuery("select distinct verse_id,text from verse_refs where filename=?", parts[1])
case len(parts) == 2 && parts[0] == "sermon":
return a.sermonQuery(sermonCols+"filename=?", parts[1])
}
return a.notFound()
}
func (a *App) sermonsByRef(ref string) Resp {
id, ok := a.refVerseID(ref)
if !ok {
return a.notFound()
}
if a.sermonDB == nil {
return jsonR(map[string]string{"error": "Sermon database not found"})
}
rows, err := a.sermonDB.Query("select * from verse_refs where verse_id=?", id)
if err != nil {
return errJSON(http.StatusInternalServerError, "Sermon database error")
}
out, err := rowsToMaps(rows)
if err != nil {
return errJSON(http.StatusInternalServerError, "Sermon database error")
}
for _, m := range out {
if vid, ok := m["verse_id"].(int64); ok {
if v := a.store.Range(vid, 0); len(v) == 1 {
m["kjv"] = v[0].Text
}
}
}
return jsonR(out)
}
// refVerseID: the verse id of the first reference in "1John5:7" style text.
func (a *App) refVerseID(ref string) (int64, bool) {
resolved := a.res.Resolve(ref, true)
if resolved == nil {
return 0, false
}
return resolved.Refs[0].Start, true
}
func (a *App) handleRef(ref string) Resp {
id, ok := a.refVerseID(ref)
if !ok {
return a.notFound()
}
return htmlR(itoa64(id))
}
func (a *App) handleNaturalPost(body io.Reader) Resp {
var req struct {
Token *string `json:"token"`
Query string `json:"query"`
}
if err := json.NewDecoder(io.LimitReader(body, 1<<16)).Decode(&req); err != nil {
return errJSON(http.StatusBadRequest, "Invalid request format")
}
if req.Token == nil {
return errJSON(http.StatusUnauthorized, "Missing token in request body")
}
if a.secretKey == "" || subtle.ConstantTimeCompare([]byte(*req.Token), []byte(a.secretKey)) != 1 {
return errJSON(http.StatusUnauthorized, "Invalid Authorization")
}
conn, err := net.DialTimeout("tcp", a.semantic, 3*time.Second)
if err != nil {
return errJSON(http.StatusBadGateway, "Semantic search is not available")
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(30 * time.Second))
payload, _ := json.Marshal(map[string]string{"query": req.Query})
if _, err := conn.Write(payload); err != nil {
return errJSON(http.StatusBadGateway, "Semantic search is not available")
}
data, err := io.ReadAll(io.LimitReader(conn, 8<<20))
var out any
if err != nil || json.Unmarshal(data, &out) != nil {
return errJSON(http.StatusBadGateway, "Semantic search gave an invalid answer")
}
return jsonR(out)
}