diff --git a/pkg/app/ask.go b/pkg/app/ask.go index 5310754..203cf46 100644 --- a/pkg/app/ask.go +++ b/pkg/app/ask.go @@ -2,6 +2,7 @@ package app import ( "fmt" + stdhtml "html" "log" "strings" @@ -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\nReferences:") 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 } diff --git a/pkg/app/ask_test.go b/pkg/app/ask_test.go index 7d79e43..c88fd1f 100644 --- a/pkg/app/ask_test.go +++ b/pkg/app/ask_test.go @@ -1,6 +1,7 @@ package app import ( + "strings" "testing" "github.com/julwrites/BotPlatform/pkg/def" @@ -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\nReferences:\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: "

God is Love

", + 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 Love") { + t.Errorf("Expected message to contain parsed HTML, got: %s", env.Res.Message) + } + if strings.Contains(env.Res.Message, "

") { + t.Errorf("Expected message to NOT contain

tag, got: %s", env.Res.Message) + } + if !strings.Contains(env.Res.Message, "References:") { + t.Errorf("Expected message to contain bold References header, got: %s", env.Res.Message) + } + }) } diff --git a/pkg/app/bible_reference.go b/pkg/app/bible_reference.go index 7d26539..0db0975 100644 --- a/pkg/app/bible_reference.go +++ b/pkg/app/bible_reference.go @@ -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 { diff --git a/pkg/app/bible_reference_test.go b/pkg/app/bible_reference_test.go index c588ad8..fdb6b46 100644 --- a/pkg/app/bible_reference_test.go +++ b/pkg/app/bible_reference_test.go @@ -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}, diff --git a/pkg/app/html_parser.go b/pkg/app/html_parser.go new file mode 100644 index 0000000..862be89 --- /dev/null +++ b/pkg/app/html_parser.go @@ -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

to newlines, handles lists, and preserves +// inline formatting like , , 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("%s", parseNodesForTelegram(child))) + case "i", "em": + parts = append(parts, fmt.Sprintf("%s", parseNodesForTelegram(child))) + case "u", "ins": + parts = append(parts, fmt.Sprintf("%s", parseNodesForTelegram(child))) + case "s", "strike", "del": + parts = append(parts, fmt.Sprintf("%s", parseNodesForTelegram(child))) + case "code": + parts = append(parts, fmt.Sprintf("%s", parseNodesForTelegram(child))) + case "pre": + parts = append(parts, fmt.Sprintf("

%s
", 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(`%s`, 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("%s\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, "") +} diff --git a/pkg/app/natural_language_test.go b/pkg/app/natural_language_test.go index fcb97d7..32b0c54 100644 --- a/pkg/app/natural_language_test.go +++ b/pkg/app/natural_language_test.go @@ -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)", }, } diff --git a/pkg/app/passage.go b/pkg/app/passage.go index 0af0998..223baf6 100644 --- a/pkg/app/passage.go +++ b/pkg/app/passage.go @@ -5,11 +5,11 @@ package app import ( "fmt" + stdhtml "html" "log" "net/url" "regexp" "strings" - stdhtml "html" "golang.org/x/net/html" @@ -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 { @@ -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{ @@ -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)