164 lines
5.1 KiB
Go
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)
|
|
}
|