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) }