Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions pkg/app/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"fmt"
stdhtml "html"
"log"
"strings"

Expand Down Expand Up @@ -50,16 +51,17 @@ func GetBibleAskWithContext(env def.SessionData, contextVerses []string) def.Ses
}

var sb strings.Builder
sb.WriteString(resp.Text)
sb.WriteString(ParseToTelegramHTML(resp.Text))

if len(resp.References) > 0 {
sb.WriteString("\n\n*References:*")
sb.WriteString("\n\n<b>References:</b>")
for _, ref := range resp.References {
sb.WriteString(fmt.Sprintf("\n- %s", ref.Verse))
sb.WriteString(fmt.Sprintf("\n %s", stdhtml.EscapeString(ref.Verse)))
}
}

env.Res.Message = sb.String()
env.Res.ParseMode = def.TELEGRAM_PARSE_MODE_HTML
}
return env
}
44 changes: 43 additions & 1 deletion pkg/app/ask_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app

import (
"strings"
"testing"

"github.com/julwrites/BotPlatform/pkg/def"
Expand Down Expand Up @@ -110,9 +111,50 @@ func TestGetBibleAsk(t *testing.T) {

env = GetBibleAsk(env)

expected := "This is a mock response.\n\n*References:*\n- John 3:16"
expected := "This is a mock response.\n\n<b>References:</b>\n• John 3:16"
if env.Res.Message != expected {
t.Errorf("Expected admin response, got: %s", env.Res.Message)
}
})

t.Run("HTML Response Handling", func(t *testing.T) {
ResetAPIConfigCache()
SetAPIConfigOverride("https://mock", "key")

// Mock SubmitQuery to return HTML
SubmitQuery = func(req QueryRequest, result interface{}) error {
if r, ok := result.(*OQueryResponse); ok {
*r = OQueryResponse{
Text: "<p>God is <b>Love</b></p>",
References: []SearchResult{
{Verse: "1 John 4:8"},
},
}
}
return nil
}

var env def.SessionData
env.Msg.Message = "Who is God?"
conf := utils.UserConfig{Version: "NIV"}
env = utils.SetUserConfig(env, utils.SerializeUserConfig(conf))

env = GetBibleAskWithContext(env, nil)

// Check ParseMode
if env.Res.ParseMode != def.TELEGRAM_PARSE_MODE_HTML {
t.Errorf("Expected ParseMode to be HTML, got %v", env.Res.ParseMode)
}

// Check Content
if !strings.Contains(env.Res.Message, "God is <b>Love</b>") {
t.Errorf("Expected message to contain parsed HTML, got: %s", env.Res.Message)
}
if strings.Contains(env.Res.Message, "<p>") {
t.Errorf("Expected message to NOT contain <p> tag, got: %s", env.Res.Message)
}
if !strings.Contains(env.Res.Message, "<b>References:</b>") {
t.Errorf("Expected message to contain bold References header, got: %s", env.Res.Message)
}
})
}
2 changes: 1 addition & 1 deletion pkg/app/bible_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func consumeReferenceSyntax(s string) (string, int) {
return "", 0
}

return s[:lastDigit+1], lastDigit+1
return s[:lastDigit+1], lastDigit + 1
}

func hasDigit(s string) bool {
Expand Down
24 changes: 12 additions & 12 deletions pkg/app/bible_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,20 @@ func TestParseBibleReference(t *testing.T) {
{"Phlm 1", "Philemon 1", true},

// Fuzzy Matches (Typos)
{"Gensis 1", "Genesis 1", true}, // Missing 'e', dist 1
{"Genisis 1", "Genesis 1", true}, // 'i' instead of 'e', dist 1
{"Mathew 5", "Matthew 5", true}, // Missing 't', dist 1
{"Revalation 3", "Revelation 3", true},// 'a' instead of 'e', dist 1
{"Philipians 4", "Philippians 4", true},// Missing 'p', dist 1
{"1 Jhn 3", "1 John 3", true}, // Missing 'o', dist 1. "1 Jhn" vs "1 John".
{"Gensis 1", "Genesis 1", true}, // Missing 'e', dist 1
{"Genisis 1", "Genesis 1", true}, // 'i' instead of 'e', dist 1
{"Mathew 5", "Matthew 5", true}, // Missing 't', dist 1
{"Revalation 3", "Revelation 3", true}, // 'a' instead of 'e', dist 1
{"Philipians 4", "Philippians 4", true}, // Missing 'p', dist 1
{"1 Jhn 3", "1 John 3", true}, // Missing 'o', dist 1. "1 Jhn" vs "1 John".

// Thresholds / False Positives
{"Genius 1", "", false}, // Dist to Genesis is > threshold? "Genius" (6) vs "Genesis" (7). Dist 3. Threshold 1. False.
{"Mary 1", "", false}, // "Mary" (4). Threshold 0. "Mark" (4). Dist 1. No fuzzy allowed for len < 5.
{"Mark 1", "Mark 1", true}, // Exact match.
{"Luke 1", "Luke 1", true}, // Exact match.
{"Luke", "Luke 1", true}, // Exact match.
{"Luek 1", "", false}, // "Luek" (4). Threshold 0. No match.
{"Genius 1", "", false}, // Dist to Genesis is > threshold? "Genius" (6) vs "Genesis" (7). Dist 3. Threshold 1. False.
{"Mary 1", "", false}, // "Mary" (4). Threshold 0. "Mark" (4). Dist 1. No fuzzy allowed for len < 5.
{"Mark 1", "Mark 1", true}, // Exact match.
{"Luke 1", "Luke 1", true}, // Exact match.
{"Luke", "Luke 1", true}, // Exact match.
{"Luek 1", "", false}, // "Luek" (4). Threshold 0. No match.

// Invalid References
{"John is here", "", false},
Expand Down
76 changes: 76 additions & 0 deletions pkg/app/html_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package app

import (
"fmt"
stdhtml "html"
"strings"

"golang.org/x/net/html"
)

// ParseToTelegramHTML converts generic HTML to Telegram-supported HTML.
// It converts block elements like <p> to newlines, handles lists, and preserves
// inline formatting like <b>, <i>, <a> while stripping unsupported tags.
func ParseToTelegramHTML(htmlStr string) string {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
// Fallback to original string if parsing fails
return htmlStr
}

return strings.TrimSpace(parseNodesForTelegram(doc))
}

func parseNodesForTelegram(node *html.Node) string {
var parts []string

for child := node.FirstChild; child != nil; child = child.NextSibling {
switch tag := child.Data; tag {
case "b", "strong":
parts = append(parts, fmt.Sprintf("<b>%s</b>", parseNodesForTelegram(child)))
case "i", "em":
parts = append(parts, fmt.Sprintf("<i>%s</i>", parseNodesForTelegram(child)))
case "u", "ins":
parts = append(parts, fmt.Sprintf("<u>%s</u>", parseNodesForTelegram(child)))
case "s", "strike", "del":
parts = append(parts, fmt.Sprintf("<s>%s</s>", parseNodesForTelegram(child)))
case "code":
parts = append(parts, fmt.Sprintf("<code>%s</code>", parseNodesForTelegram(child)))
case "pre":
parts = append(parts, fmt.Sprintf("<pre>%s</pre>", parseNodesForTelegram(child)))
case "a":
href := ""
for _, attr := range child.Attr {
if attr.Key == "href" {
href = attr.Val
break
}
}
if href != "" {
parts = append(parts, fmt.Sprintf(`<a href="%s">%s</a>`, href, parseNodesForTelegram(child)))
} else {
parts = append(parts, parseNodesForTelegram(child))
}
case "p":
parts = append(parts, parseNodesForTelegram(child))
parts = append(parts, "\n\n")
case "br":
parts = append(parts, "\n")
case "ul", "ol":
parts = append(parts, parseNodesForTelegram(child))
case "li":
parts = append(parts, fmt.Sprintf("• %s\n", strings.TrimSpace(parseNodesForTelegram(child))))
case "h1", "h2", "h3", "h4", "h5", "h6":
parts = append(parts, fmt.Sprintf("<b>%s</b>\n", strings.TrimSpace(parseNodesForTelegram(child))))
default:
if child.Type == html.TextNode {
parts = append(parts, stdhtml.EscapeString(child.Data))
} else if child.Type == html.ElementNode {
// Recurse for unknown elements (like div, span) to preserve content
parts = append(parts, parseNodesForTelegram(child))
}
}
}

return strings.Join(parts, "")
}
68 changes: 36 additions & 32 deletions pkg/app/natural_language_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,78 +24,82 @@ func TestProcessNaturalLanguage(t *testing.T) {
{
name: "Passage: Reference",
message: "John 3:16",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "John 3") || strings.Contains(msg, "loved the world") },
expectedCheck: func(msg string) bool {
return strings.Contains(msg, "John 3") || strings.Contains(msg, "loved the world")
},
desc: "Should retrieve John 3:16 passage",
},
{
name: "Passage: Short Book",
message: "Jude",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "Jude") || strings.Contains(msg, "servant of Jesus Christ") },
expectedCheck: func(msg string) bool {
return strings.Contains(msg, "Jude") || strings.Contains(msg, "servant of Jesus Christ")
},
desc: "Should retrieve Jude passage",
},
{
name: "Passage: Book only",
message: "Genesis",
name: "Passage: Book only",
message: "Genesis",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "Genesis 1") },
desc: "Should retrieve Genesis 1",
desc: "Should retrieve Genesis 1",
},

// Search Scenarios
{
name: "Search: One word",
message: "Grace",
name: "Search: One word",
message: "Grace",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "Found") || strings.Contains(msg, "No results") },
desc: "Should perform search for Grace",
desc: "Should perform search for Grace",
},
{
name: "Search: Short phrase",
message: "Jesus wept",
name: "Search: Short phrase",
message: "Jesus wept",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "Found") },
desc: "Should perform search for Jesus wept",
desc: "Should perform search for Jesus wept",
},
{
name: "Search: 3 words",
message: "Love of God",
name: "Search: 3 words",
message: "Love of God",
expectedCheck: func(msg string) bool { return strings.Contains(msg, "Found") },
desc: "Should perform search for Love of God",
desc: "Should perform search for Love of God",
},

// Ask Scenarios
{
name: "Ask: Question",
message: "What does the bible say about love?",
name: "Ask: Question",
message: "What does the bible say about love?",
expectedCheck: func(msg string) bool { return len(msg) == 0 },
desc: "Should not ask the AI (Question)",
desc: "Should not ask the AI (Question)",
},
{
name: "Ask: With Reference",
message: "Explain John 3:16",
name: "Ask: With Reference",
message: "Explain John 3:16",
expectedCheck: func(msg string) bool { return !strings.Contains(msg, "Found") },
desc: "Should ask the AI (With Reference)",
desc: "Should ask the AI (With Reference)",
},
{
name: "Ask: Compare",
message: "Compare Genesis 1 and John 1",
name: "Ask: Compare",
message: "Compare Genesis 1 and John 1",
expectedCheck: func(msg string) bool { return true },
desc: "Should ask the AI (Compare)",
desc: "Should ask the AI (Compare)",
},
{
name: "Ask: Short Question",
message: "Who is Jesus?",
name: "Ask: Short Question",
message: "Who is Jesus?",
expectedCheck: func(msg string) bool { return len(msg) == 0 && !strings.Contains(msg, "Found") },
desc: "Should not ask the AI (Short Question)",
desc: "Should not ask the AI (Short Question)",
},
{
name: "Ask: Embedded Reference",
message: "What does it say in Mark 5?",
name: "Ask: Embedded Reference",
message: "What does it say in Mark 5?",
expectedCheck: func(msg string) bool { return true },
desc: "Should ask the AI (Embedded Reference)",
desc: "Should ask the AI (Embedded Reference)",
},
{
name: "Ask: Book name in text",
message: "I like Genesis",
name: "Ask: Book name in text",
message: "I like Genesis",
expectedCheck: func(msg string) bool { return !strings.Contains(msg, "Found") },
desc: "Should ask the AI (Found reference Genesis)",
desc: "Should ask the AI (Found reference Genesis)",
},
}

Expand Down
7 changes: 3 additions & 4 deletions pkg/app/passage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ package app

import (
"fmt"
stdhtml "html"
"log"
"net/url"
"regexp"
"strings"
stdhtml "html"

"golang.org/x/net/html"

Expand Down Expand Up @@ -43,7 +43,6 @@ func GetReference(doc *html.Node) string {
return utils.GetTextNode(refNode).Data
}


func isNextSiblingBr(node *html.Node) bool {
for next := node.NextSibling; next != nil; next = next.NextSibling {
if next.Type == html.TextNode {
Expand Down Expand Up @@ -234,7 +233,7 @@ func GetBiblePassage(env def.SessionData) def.SessionData {

// If indeed a reference, attempt to query
if len(ref) > 0 {
log.Printf("%s", ref);
log.Printf("%s", ref)

// Attempt to retrieve from API
req := QueryRequest{
Expand All @@ -256,7 +255,7 @@ func GetBiblePassage(env def.SessionData) def.SessionData {
log.Printf("Error retrieving passage from API: %v. Falling back to deprecated method.", err)

return GetBiblePassageFallback(env)
}
}

if len(resp.Verse) > 0 {
env.Res.Message = ParsePassageFromHtml(env.Msg.Message, resp.Verse, config.Version)
Expand Down
Loading