├── Dockerfile ├── module ├── database │ ├── view.go │ ├── schema.go │ └── query.go ├── global │ ├── bible.go │ ├── translation.go │ └── book.go └── operation │ ├── load_bible_json.go │ ├── run_server.go │ ├── get_commands.go │ ├── pull.go │ ├── make_bible_json.go │ └── make_bible_db.go ├── go.mod ├── main.go ├── README.md ├── .gitignore └── go.sum /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.3-bookworm 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod tidy 7 | 8 | COPY . . 9 | 10 | RUN go build -v -o /usr/local/bin/app . 11 | 12 | EXPOSE 8080 13 | 14 | CMD ["/usr/local/bin/app"] 15 | -------------------------------------------------------------------------------- /module/database/view.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/jmoiron/sqlx" 4 | 5 | func ViewTables(db *sqlx.DB) ([]string, error) { 6 | var tables []string 7 | err := db.Select(&tables, "SELECT name FROM sqlite_master WHERE type='table'") 8 | if err != nil { 9 | return tables, err 10 | } 11 | return tables, nil 12 | } 13 | -------------------------------------------------------------------------------- /module/global/bible.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "fmt" 4 | 5 | const BIBLE_SRC = "https://bible.com/bible" 6 | 7 | func GetBibleUrl(book Book, translation Translation, chapter int) string { 8 | return fmt.Sprintf(`%s/%d/%s.%d.%s`, BIBLE_SRC, translation.PageCode, book.Abbreviation, chapter, translation.Abbreviation) 9 | } 10 | 11 | func GetBibleOut(out string, book Book, translation Translation) string { 12 | return fmt.Sprintf(`%s/%s/%s`, out, translation.Abbreviation, book.Abbreviation) 13 | } 14 | -------------------------------------------------------------------------------- /module/global/translation.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | type Translation struct { 4 | Abbreviation string 5 | Name string 6 | PageCode int32 7 | } 8 | 9 | func GetBibleTranslations() []Translation { 10 | return []Translation{ 11 | {"KJV", "King James Version", 1}, 12 | {"ASV", "American Standard Version", 12}, 13 | {"WEBUS", "World English Bible", 206}, 14 | } 15 | } 16 | 17 | func GetTranslationByAbbreviation(abbr string) (Translation, bool) { 18 | for _, t := range GetBibleTranslations() { 19 | if t.Abbreviation == abbr { 20 | return t, true 21 | } 22 | } 23 | return Translation{}, false 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Phillip-England/bible-bot 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/Phillip-England/vbf v0.1.0 7 | github.com/PuerkitoBio/goquery v1.10.2 8 | github.com/jmoiron/sqlx v1.4.0 9 | github.com/mattn/go-sqlite3 v1.14.24 10 | ) 11 | 12 | require ( 13 | github.com/a-h/templ v0.2.771 // indirect 14 | github.com/alecthomas/chroma/v2 v2.14.0 // indirect 15 | github.com/andybalholm/cascadia v1.3.3 // indirect 16 | github.com/dlclark/regexp2 v1.11.0 // indirect 17 | github.com/yuin/goldmark v1.7.4 // indirect 18 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect 19 | golang.org/x/net v0.35.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /module/operation/load_bible_json.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func LoadBibleJson(src string) []Verse { 11 | var verses []Verse 12 | err := filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { 13 | if info.IsDir() { 14 | return nil 15 | } 16 | fileBytes, err := os.ReadFile(path) 17 | if err != nil { 18 | return err 19 | } 20 | verse := &Verse{} 21 | err = json.Unmarshal(fileBytes, verse) 22 | if err != nil { 23 | return err 24 | } 25 | verses = append(verses, *verse) 26 | return nil 27 | }) 28 | if err != nil { 29 | panic(err) 30 | } 31 | return verses 32 | } 33 | -------------------------------------------------------------------------------- /module/database/schema.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type Translation struct { 4 | ID uint `db:"id"` 5 | Translation string `db:"translation"` 6 | Abbreviation string `db:"abbreviation"` 7 | } 8 | 9 | type Book struct { 10 | ID uint `db:"id"` 11 | TranslationID uint `db:"translation_id"` 12 | Name string `db:"name"` 13 | Abbreviation string `db:"abbreviation"` 14 | } 15 | 16 | type Chapter struct { 17 | ID uint `db:"id"` 18 | BookID uint `db:"book_id"` 19 | Number int `db:"number"` 20 | } 21 | 22 | type Verse struct { 23 | ID uint `db:"id"` 24 | ChapterID uint `db:"chapter_id"` 25 | Number int `db:"number"` 26 | Text string `db:"text"` 27 | Section string `db:"section"` 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Phillip-England/bible-bot/module/operation" 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | func main() { 11 | 12 | // scape and generate the db 13 | err := operation.Pull("bible_html", 100) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | err = operation.GetCommands("bible_html", "bible_commands") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | err = operation.MakeBibleJson("bible_commands", "bible_json") 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | err = operation.MakeBibleDb("bible_json") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // serve the results 31 | err = operation.RunServer() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /module/operation/run_server.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/Phillip-England/bible-bot/module/database" 9 | "github.com/Phillip-England/vbf" 10 | "github.com/jmoiron/sqlx" 11 | ) 12 | 13 | func RunServer() error { 14 | db, err := sqlx.Open("sqlite3", "bible.db") 15 | if err != nil { 16 | return err 17 | } 18 | defer db.Close() 19 | 20 | mux, gCtx := vbf.VeryBestFramework() 21 | 22 | gCtx["DB"] = db 23 | 24 | vbf.AddRoute("GET /{translation}/{book}/{chapter}/{verse}", mux, gCtx, func(w http.ResponseWriter, r *http.Request) { 25 | 26 | db, ok := gCtx["DB"].(*sqlx.DB) 27 | if !ok { 28 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 29 | return 30 | } 31 | 32 | trans := r.PathValue("translation") 33 | book := r.PathValue("book") 34 | chapterStr := r.PathValue("chapter") 35 | verseStr := r.PathValue("verse") 36 | 37 | chapterNum, err := strconv.Atoi(chapterStr) 38 | if err != nil { 39 | vbf.WriteJSON(w, 400, map[string]interface{}{ 40 | "message": "chapter must be a valid number", 41 | }) 42 | return 43 | } 44 | 45 | verseNum, err := strconv.Atoi(verseStr) 46 | if err != nil { 47 | vbf.WriteJSON(w, 400, map[string]interface{}{ 48 | "message": "verse must be a valid number", 49 | }) 50 | return 51 | } 52 | 53 | verse, err := database.GetVerse(db, trans, book, uint(chapterNum), uint(verseNum)) 54 | if err != nil { 55 | vbf.WriteJSON(w, 400, map[string]interface{}{ 56 | "message": fmt.Sprintf(`verse number %d does not exist`, verseNum), 57 | }) 58 | return 59 | } 60 | 61 | vbf.WriteJSON(w, 200, verse) 62 | }, vbf.MwCORS, vbf.MwLogger) 63 | 64 | err = vbf.Serve(mux, "8080") 65 | if err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bible-bot 2 | Webscape the KJV, WEBUS, and ASV versions of the bible, stash the verses into a sqlite database, and serve them via an API. 3 | 4 | ## Installation 5 | To install, run: 6 | ```bash 7 | git clone https://github.com/phillip-england/bible-bot 8 | ``` 9 | 10 | ## Scraping, DB Generation, and Serving 11 | Start the program to scape The Bible from https://bible.com (this will take a few minutes), save the results in an sqlite db, and serve them on port 8080: 12 | ```bash 13 | cd bible-bot 14 | go run main.go 15 | ``` 16 | 17 | ## Accessing the Verses 18 | The API is structured as follows: 19 | ```bash 20 | /TRANSLATION/BOOK/CHAPTER/VERSE 21 | ``` 22 | 23 | To access Genesis 1:1 from the KJV, run (make sure the serve is running on port 8080): 24 | ```bash 25 | curl localhost:8080/KJV/GEN/1/1 26 | ``` 27 | 28 | ## Book Names 29 | Each book of The Bible has an abbreviated name, here are the abbreviations: 30 | ```bash 31 | Genesis:GEN 32 | Exodus:EXO 33 | Leviticus:LEV 34 | Numbers:NUM 35 | Deuteronomy:DEU 36 | Joshua:JOS 37 | Judges:JDG 38 | Ruth:RUT 39 | 1 Samuel:1SA 40 | 2 Samuel:2SA 41 | 1 Kings:1KI 42 | 2 Kings:2KI 43 | 1 Chronicles:1CH 44 | 2 Chronicles:2CH 45 | Ezra:EZR 46 | Nehemiah:NEH 47 | Esther:EST 48 | Job:JOB 49 | Psalms:PSA 50 | Proverbs:PRO 51 | Ecclesiastes:ECC 52 | Song of Solomon:SNG 53 | Isaiah:ISA 54 | Jeremiah:JER 55 | Lamentations:LAM 56 | Ezekiel:EZE 57 | Daniel:DAN 58 | Hosea:HOS 59 | Joel:JOL 60 | Amos:AMO 61 | Obadiah:OBA 62 | Jonah:JON 63 | Micah:MIC 64 | Nahum:NAH 65 | Habakkuk:HAB 66 | Zephaniah:ZEP 67 | Haggai:HAG 68 | Zechariah:ZEC 69 | Malachi:MAL 70 | Matthew:MAT 71 | Mark:MAR 72 | Luke:LUK 73 | John:JHN 74 | Acts:ACT 75 | Romans:ROM 76 | 1 Corinthians:1CO 77 | 2 Corinthians:2CO 78 | Galatians:GAL 79 | Ephesians:EPH 80 | Philippians:PHP 81 | Colossians:COL 82 | 1 Thessalonians:1TH 83 | 2 Thessalonians:2TH 84 | 1 Timothy:1TI 85 | 2 Timothy:2TI 86 | Titus:TIT 87 | Philemon:PHM 88 | Hebrews:HEB 89 | James:JAS 90 | 1 Peter:1PE 91 | 2 Peter:2PE 92 | 1 John:1JN 93 | 2 John:2JN 94 | 3 John:3JN 95 | Jude:JUD 96 | Revelation:REV 97 | ``` -------------------------------------------------------------------------------- /module/global/book.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | type Book struct { 4 | Abbreviation string 5 | Name string 6 | } 7 | 8 | func GetBibleBooks() []Book { 9 | return []Book{ 10 | {"GEN", "Genesis"}, 11 | {"EXO", "Exodus"}, 12 | {"LEV", "Leviticus"}, 13 | {"NUM", "Numbers"}, 14 | {"DEU", "Deuteronomy"}, 15 | {"JOS", "Joshua"}, 16 | {"JDG", "Judges"}, 17 | {"RUT", "Ruth"}, 18 | {"1SA", "1 Samuel"}, 19 | {"2SA", "2 Samuel"}, 20 | {"1KI", "1 Kings"}, 21 | {"2KI", "2 Kings"}, 22 | {"1CH", "1 Chronicles"}, 23 | {"2CH", "2 Chronicles"}, 24 | {"EZR", "Ezra"}, 25 | {"NEH", "Nehemiah"}, 26 | {"EST", "Esther"}, 27 | {"JOB", "Job"}, 28 | {"PSA", "Psalms"}, 29 | {"PRO", "Proverbs"}, 30 | {"ECC", "Ecclesiastes"}, 31 | {"SNG", "Song of Solomon"}, 32 | {"ISA", "Isaiah"}, 33 | {"JER", "Jeremiah"}, 34 | {"LAM", "Lamentations"}, 35 | {"EZK", "Ezekiel"}, 36 | {"DAN", "Daniel"}, 37 | {"HOS", "Hosea"}, 38 | {"JOL", "Joel"}, 39 | {"AMO", "Amos"}, 40 | {"OBA", "Obadiah"}, 41 | {"JON", "Jonah"}, 42 | {"MIC", "Micah"}, 43 | {"NAM", "Nahum"}, 44 | {"HAB", "Habakkuk"}, 45 | {"ZEP", "Zephaniah"}, 46 | {"HAG", "Haggai"}, 47 | {"ZEC", "Zechariah"}, 48 | {"MAL", "Malachi"}, 49 | {"MAT", "Matthew"}, 50 | {"MRK", "Mark"}, 51 | {"LUK", "Luke"}, 52 | {"JHN", "John"}, 53 | {"ACT", "Acts"}, 54 | {"ROM", "Romans"}, 55 | {"1CO", "1 Corinthians"}, 56 | {"2CO", "2 Corinthians"}, 57 | {"GAL", "Galatians"}, 58 | {"EPH", "Ephesians"}, 59 | {"PHP", "Philippians"}, 60 | {"COL", "Colossians"}, 61 | {"1TH", "1 Thessalonians"}, 62 | {"2TH", "2 Thessalonians"}, 63 | {"1TI", "1 Timothy"}, 64 | {"2TI", "2 Timothy"}, 65 | {"TIT", "Titus"}, 66 | {"PHM", "Philemon"}, 67 | {"HEB", "Hebrews"}, 68 | {"JAS", "James"}, 69 | {"1PE", "1 Peter"}, 70 | {"2PE", "2 Peter"}, 71 | {"1JN", "1 John"}, 72 | {"2JN", "2 John"}, 73 | {"3JN", "3 John"}, 74 | {"JUD", "Jude"}, 75 | {"REV", "Revelation"}, 76 | } 77 | } 78 | 79 | func GetBookByAbbreviation(abbr string) (Book, bool) { 80 | for _, b := range GetBibleBooks() { 81 | if b.Abbreviation == abbr { 82 | return b, true 83 | } 84 | } 85 | return Book{}, false 86 | } 87 | -------------------------------------------------------------------------------- /module/operation/get_commands.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/PuerkitoBio/goquery" 11 | ) 12 | 13 | func GetCommands(bibleSrc string, out string) error { 14 | if err := os.RemoveAll(out); err != nil { 15 | return err 16 | } 17 | filepath.Walk(bibleSrc, func(path string, info fs.FileInfo, err error) error { 18 | if info.IsDir() { 19 | return nil 20 | } 21 | pathParts := strings.Split(path, "/") 22 | t := pathParts[1] 23 | b := pathParts[2] 24 | v := strings.Replace(pathParts[3], ".html", "", 1) 25 | fmt.Println(t, b, v) 26 | fileHtml, err := os.ReadFile(path) 27 | if err != nil { 28 | return err 29 | } 30 | doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(fileHtml))) 31 | if err != nil { 32 | return err 33 | } 34 | doc.Find(".ChapterContent_note__YlDW0").Each(func(i int, s *goquery.Selection) { 35 | s.Remove() 36 | }) 37 | commands := []string{ 38 | "TRANSLATION:" + t, 39 | "BOOK:" + b, 40 | "CHAPTER:" + v, 41 | } 42 | doc.Find("*").Each(func(i int, s *goquery.Selection) { 43 | 44 | className, _ := s.Attr("class") 45 | text := strings.TrimSpace(s.Text()) 46 | dataUsfm, usfmExists := s.Attr("data-usfm") 47 | if text == "" { 48 | return 49 | } 50 | if goquery.NodeName(s) == "h1" { 51 | commands = append(commands, "TITLE:"+text) 52 | } 53 | 54 | if className == "ChapterContent_heading__xBDcs" { 55 | commands = append(commands, "SECTION:"+text) 56 | } 57 | 58 | if usfmExists { 59 | parts := strings.Split(dataUsfm, ".") 60 | if len(parts) == 2 { 61 | return 62 | } 63 | verseNumber := parts[len(parts)-1] 64 | label := "LABEL:" + verseNumber 65 | foundLabel := false 66 | for _, value := range commands { 67 | if value == label { 68 | foundLabel = true 69 | break 70 | } 71 | } 72 | if !foundLabel { 73 | commands = append(commands, label) 74 | } 75 | text = strings.Replace(text, verseNumber, "", 1) 76 | commands = append(commands, "VERSE:"+text) 77 | } 78 | 79 | }) 80 | bibleSrc = strings.ReplaceAll(bibleSrc, "./", "") 81 | outPath := strings.ReplaceAll(path, bibleSrc, out) 82 | outPath = strings.ReplaceAll(outPath, ".html", ".txt") 83 | parts := strings.Split(outPath, "/") 84 | dir := strings.Join(parts[:len(parts)-1], "/") 85 | err = os.MkdirAll(dir, 0755) 86 | if err != nil { 87 | return err 88 | } 89 | fmt.Println("writing commands to disk at: " + outPath) 90 | err = os.WriteFile(outPath, []byte(strings.Join(commands, "\n")), 0755) 91 | if err != nil { 92 | return err 93 | } 94 | return nil 95 | }) 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /module/operation/pull.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/Phillip-England/bible-bot/module/global" 12 | "github.com/PuerkitoBio/goquery" 13 | ) 14 | 15 | func Pull(out string, maxConcurrent int) error { 16 | if err := os.RemoveAll(out); err != nil { 17 | return err 18 | } 19 | sem := make(chan struct{}, maxConcurrent) 20 | var wg sync.WaitGroup 21 | errs := make(chan error) 22 | var once sync.Once 23 | 24 | for _, b := range global.GetBibleBooks() { 25 | for _, t := range global.GetBibleTranslations() { 26 | for chapter := 1; chapter < 160; chapter++ { 27 | book := b 28 | translation := t 29 | chap := chapter 30 | url := global.GetBibleUrl(book, translation, chap) 31 | dir := global.GetBibleOut(out, book, translation) 32 | outHtml := fmt.Sprintf("%s/%d.html", dir, chap) 33 | select { 34 | case err := <-errs: 35 | return err 36 | default: 37 | } 38 | wg.Add(1) 39 | sem <- struct{}{} 40 | go func(url, dir, outHtml string) { 41 | defer wg.Done() 42 | defer func() { <-sem }() 43 | handleError := func(err error) { 44 | if err != nil { 45 | once.Do(func() { 46 | errs <- err 47 | }) 48 | } 49 | } 50 | fmt.Println("requesting:", url) 51 | resp, err := http.Get(url) 52 | if err != nil { 53 | handleError(fmt.Errorf("error requesting %s: %w", url, err)) 54 | return 55 | } 56 | defer resp.Body.Close() 57 | body, err := io.ReadAll(resp.Body) 58 | if err != nil { 59 | handleError(fmt.Errorf("error reading response body for %s: %w", url, err)) 60 | return 61 | } 62 | doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) 63 | if err != nil { 64 | handleError(fmt.Errorf("error creating goquery document for %s: %w", url, err)) 65 | return 66 | } 67 | notFoundSel := doc.Find(".ChapterContent_not-avaliable-span__WrOM_") 68 | if notFoundSel.Length() > 0 { 69 | return 70 | } 71 | contentSel := doc.Find(".ChapterContent_yv-bible-text__tqVMm") 72 | bodyHtml, err := contentSel.Html() 73 | if err != nil { 74 | handleError(fmt.Errorf("error getting HTML content for %s: %w", url, err)) 75 | return 76 | } 77 | if err := os.MkdirAll(dir, 0755); err != nil { 78 | handleError(fmt.Errorf("error creating directory %s: %w", dir, err)) 79 | return 80 | } 81 | fmt.Println("writing to:", outHtml) 82 | if err := os.WriteFile(outHtml, []byte(bodyHtml), 0644); err != nil { 83 | handleError(fmt.Errorf("error writing file %s: %w", outHtml, err)) 84 | return 85 | } 86 | }(url, dir, outHtml) 87 | } 88 | } 89 | } 90 | go func() { 91 | wg.Wait() 92 | close(errs) 93 | }() 94 | if err, ok := <-errs; ok { 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /module/operation/make_bible_json.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/Phillip-England/bible-bot/module/global" 13 | ) 14 | 15 | type Verse struct { 16 | Number int `json:"number"` 17 | Text string `json:"text"` 18 | Section string `json:"section"` 19 | Translation string `json:"translation"` 20 | TranslationAbbv string `json:"translation_abbreviation"` 21 | Book string `json:"book"` 22 | BookAbbv string `json:"book_abbreviation"` 23 | Chapter int `json:"chapter"` 24 | } 25 | 26 | func MakeBibleJson(commandSrc string, out string) error { 27 | err := os.RemoveAll(out) 28 | if err != nil { 29 | return err 30 | } 31 | var verses []Verse 32 | err = filepath.Walk(commandSrc, func(path string, info fs.FileInfo, err error) error { 33 | if info.IsDir() { 34 | return nil 35 | } 36 | fileBytes, err := os.ReadFile(path) 37 | if err != nil { 38 | return err 39 | } 40 | fileStr := string(fileBytes) 41 | commands := strings.Split(fileStr, "\n") 42 | verse := &Verse{} 43 | currentVerseNumber := "" 44 | for _, command := range commands { 45 | parts := strings.Split(command, ":") 46 | commandName := parts[0] 47 | commandValueParts := parts[1:len(parts)] 48 | commandValue := strings.Join(commandValueParts, ":") 49 | if commandName == "TRANSLATION" { 50 | verse.TranslationAbbv = commandValue 51 | } 52 | if commandName == "BOOK" { 53 | verse.BookAbbv = commandValue 54 | } 55 | if commandName == "CHAPTER" { 56 | chapterNum, err := strconv.Atoi(commandValue) 57 | if err != nil { 58 | return err 59 | } 60 | verse.Chapter = chapterNum 61 | } 62 | if commandName == "SECTION" { 63 | verse.Section = commandValue 64 | } 65 | if commandName == "LABEL" { 66 | currentVerseNumber = commandValue 67 | } 68 | if commandName == "VERSE" { 69 | verseNum, err := strconv.Atoi(currentVerseNumber) 70 | if err != nil { 71 | return err 72 | } 73 | verse.Number = verseNum 74 | verse.Text = commandValue 75 | } 76 | if verse.BookAbbv != "" && verse.Chapter != 0 && verse.Number != 0 && verse.Text != "" && verse.TranslationAbbv != "" { 77 | book, _ := global.GetBookByAbbreviation(verse.BookAbbv) 78 | verse.Book = book.Name 79 | trans, _ := global.GetTranslationByAbbreviation(verse.TranslationAbbv) 80 | verse.Translation = trans.Name 81 | verses = append(verses, *verse) 82 | } 83 | } 84 | return nil 85 | }) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | for _, verse := range verses { 91 | jsonData, err := json.MarshalIndent(verse, "", " ") 92 | if err != nil { 93 | return err 94 | } 95 | outDir := fmt.Sprintf(`%s/%s/%s/%d`, out, verse.TranslationAbbv, verse.BookAbbv, verse.Chapter) 96 | out := fmt.Sprintf(`%s/%d.json`, outDir, verse.Number) 97 | fmt.Println(out) 98 | err = os.MkdirAll(outDir, 0755) 99 | if err != nil { 100 | return err 101 | } 102 | err = os.WriteFile(out, jsonData, 0644) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /module/database/query.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | ) 6 | 7 | func GetAllTranslation(db *sqlx.DB) ([]Translation, error) { 8 | var translations []Translation 9 | err := db.Select(&translations, "SELECT id, translation, abbreviation FROM translation") 10 | if err != nil { 11 | return translations, err 12 | } 13 | return translations, nil 14 | } 15 | 16 | func GetTranslationIDByAbbv(db *sqlx.DB, translationAbbv string) (uint, error) { 17 | var transID uint 18 | err := db.Get(&transID, "SELECT id FROM translation WHERE abbreviation = ?", translationAbbv) 19 | if err != nil { 20 | return transID, err 21 | } 22 | return transID, nil 23 | } 24 | 25 | func GetBookIDByName(db *sqlx.DB, bookName string) (uint, error) { 26 | var bookID uint 27 | err := db.Get(&bookID, "SELECT id FROM book WHERE name = ?", bookName) 28 | if err != nil { 29 | return bookID, err 30 | } 31 | return bookID, nil 32 | } 33 | 34 | func GetBookIDByAbbv(db *sqlx.DB, bookAbbv string) (uint, error) { 35 | var bookID uint 36 | err := db.Get(&bookID, "SELECT id FROM book WHERE abbreviation = ?", bookAbbv) 37 | if err != nil { 38 | return bookID, err 39 | } 40 | return bookID, nil 41 | } 42 | 43 | func GetChapterID(db *sqlx.DB, bookID uint, chapterNumber uint) (uint, error) { 44 | var chapterID uint 45 | err := db.Get(&chapterID, "SELECT id FROM chapter WHERE book_id = ? AND number = ?", bookID, chapterNumber) 46 | if err != nil { 47 | return chapterID, err 48 | } 49 | return chapterID, nil 50 | } 51 | 52 | func GetVerseByChapterID(db *sqlx.DB, chapterID uint, verseNumber uint) (Verse, error) { 53 | var verse Verse 54 | err := db.Get(&verse, "SELECT id, chapter_id, number, text, section FROM verse WHERE chapter_id = ? AND number = ?", chapterID, verseNumber) 55 | if err != nil { 56 | return verse, err 57 | } 58 | return verse, nil 59 | } 60 | 61 | func GetVerse(db *sqlx.DB, translationAbbv string, bookAbbv string, chapterNumber uint, verseNumber uint) (Verse, error) { 62 | var verse Verse 63 | query := ` 64 | SELECT v.id, v.chapter_id, v.number, v.text, v.section 65 | FROM verse v 66 | JOIN chapter c ON v.chapter_id = c.id 67 | JOIN book b ON c.book_id = b.id 68 | JOIN translation t ON b.translation_id = t.id 69 | WHERE t.abbreviation = ? 70 | AND b.abbreviation = ? 71 | AND c.number = ? 72 | AND v.number = ?; 73 | ` 74 | err := db.Get(&verse, query, translationAbbv, bookAbbv, chapterNumber, verseNumber) 75 | if err != nil { 76 | return verse, err 77 | } 78 | return verse, nil 79 | } 80 | 81 | func GetChapterWithVerses(db *sqlx.DB, translationAbbv string, bookAbbv string, chapterNumber uint) (Chapter, []Verse, error) { 82 | var chapter Chapter 83 | var verses []Verse 84 | 85 | query := ` 86 | SELECT c.id, c.book_id, c.number 87 | FROM chapter c 88 | JOIN book b ON c.book_id = b.id 89 | JOIN translation t ON b.translation_id = t.id 90 | WHERE t.abbreviation = ? AND b.abbreviation = ? AND c.number = ? 91 | LIMIT 1; 92 | ` 93 | err := db.Get(&chapter, query, translationAbbv, bookAbbv, chapterNumber) 94 | if err != nil { 95 | return chapter, verses, err 96 | } 97 | 98 | queryVerses := ` 99 | SELECT v.id, v.chapter_id, v.number, v.text, v.section 100 | FROM verse v 101 | WHERE v.chapter_id = ? 102 | ORDER BY v.number; 103 | ` 104 | err = db.Select(&verses, queryVerses, chapter.ID) 105 | if err != nil { 106 | return chapter, verses, err 107 | } 108 | 109 | return chapter, verses, nil 110 | } 111 | 112 | func GetBookWithChaptersAndVerses(db *sqlx.DB, translationAbbv string, bookAbbv string) (Book, []Chapter, map[uint][]Verse, error) { 113 | var book Book 114 | var chapters []Chapter 115 | versesMap := make(map[uint][]Verse) 116 | 117 | queryBook := ` 118 | SELECT b.id, b.translation_id, b.name, b.abbreviation 119 | FROM book b 120 | JOIN translation t ON b.translation_id = t.id 121 | WHERE t.abbreviation = ? AND b.abbreviation = ? 122 | LIMIT 1; 123 | ` 124 | err := db.Get(&book, queryBook, translationAbbv, bookAbbv) 125 | if err != nil { 126 | return book, chapters, versesMap, err 127 | } 128 | 129 | queryChapters := ` 130 | SELECT c.id, c.book_id, c.number 131 | FROM chapter c 132 | WHERE c.book_id = ? 133 | ORDER BY c.number; 134 | ` 135 | err = db.Select(&chapters, queryChapters, book.ID) 136 | if err != nil { 137 | return book, chapters, versesMap, err 138 | } 139 | 140 | queryVerses := ` 141 | SELECT v.id, v.chapter_id, v.number, v.text, v.section 142 | FROM verse v 143 | WHERE v.chapter_id = ? 144 | ORDER BY v.number; 145 | ` 146 | 147 | for _, chapter := range chapters { 148 | var verses []Verse 149 | err = db.Select(&verses, queryVerses, chapter.ID) 150 | if err != nil { 151 | return book, chapters, versesMap, err 152 | } 153 | versesMap[chapter.ID] = verses 154 | } 155 | 156 | return book, chapters, versesMap, nil 157 | } 158 | -------------------------------------------------------------------------------- /module/operation/make_bible_db.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Phillip-England/bible-bot/module/database" 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | const schema = ` 12 | CREATE TABLE IF NOT EXISTS translation ( 13 | id INTEGER PRIMARY KEY AUTOINCREMENT, 14 | translation TEXT NOT NULL, 15 | abbreviation TEXT UNIQUE NOT NULL 16 | ); 17 | 18 | CREATE TABLE IF NOT EXISTS book ( 19 | id INTEGER PRIMARY KEY AUTOINCREMENT, 20 | translation_id INTEGER NOT NULL, 21 | name TEXT NOT NULL, 22 | abbreviation TEXT NOT NULL, 23 | FOREIGN KEY (translation_id) REFERENCES translation(id) ON DELETE CASCADE 24 | ); 25 | 26 | CREATE TABLE IF NOT EXISTS chapter ( 27 | id INTEGER PRIMARY KEY AUTOINCREMENT, 28 | book_id INTEGER NOT NULL, 29 | number INTEGER NOT NULL, 30 | FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE 31 | ); 32 | 33 | CREATE TABLE IF NOT EXISTS verse ( 34 | id INTEGER PRIMARY KEY AUTOINCREMENT, 35 | chapter_id INTEGER NOT NULL, 36 | number INTEGER NOT NULL, 37 | text TEXT NOT NULL, 38 | section TEXT NOT NULL, 39 | FOREIGN KEY (chapter_id) REFERENCES chapter(id) ON DELETE CASCADE 40 | );` 41 | 42 | func MakeBibleDb(bibleJsonSrc string) error { 43 | 44 | fmt.Println("Loading db...") 45 | db, err := sqlx.Open("sqlite3", "./bible.db") 46 | if err != nil { 47 | return err 48 | } 49 | defer db.Close() 50 | 51 | fmt.Println("Creating sql tables...") 52 | _, err = db.Exec(schema) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | fmt.Println("Indexing the database for better performance...") 58 | _, err = db.Exec(` 59 | CREATE INDEX IF NOT EXISTS idx_book_translation ON book (translation_id); 60 | CREATE INDEX IF NOT EXISTS idx_book_name ON book (name); 61 | CREATE INDEX IF NOT EXISTS idx_chapter_book ON chapter (book_id); 62 | CREATE INDEX IF NOT EXISTS idx_chapter_number ON chapter (book_id, number); 63 | CREATE INDEX IF NOT EXISTS idx_verse_chapter ON verse (chapter_id); 64 | CREATE INDEX IF NOT EXISTS idx_verse_number ON verse (chapter_id, number); 65 | `) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | fmt.Println("Loading in verses...") 71 | verses := LoadBibleJson(bibleJsonSrc) 72 | 73 | for _, verse := range verses { 74 | 75 | var t database.Translation 76 | var b database.Book 77 | var ch database.Chapter 78 | 79 | fmt.Printf("Checking if the %s translation exists...\n", verse.TranslationAbbv) 80 | err = db.Get(&t, `SELECT * FROM translation WHERE abbreviation = ?`, verse.TranslationAbbv) 81 | if err != nil { 82 | fmt.Printf("The %s translation does not exist, creating it...\n", verse.TranslationAbbv) 83 | tQuery := `INSERT INTO translation (translation, abbreviation) VALUES (?, ?) RETURNING id` 84 | err = db.QueryRow(tQuery, verse.Translation, verse.TranslationAbbv).Scan(&t.ID) 85 | if err != nil { 86 | return err 87 | } 88 | fmt.Printf("Created translation %s with ID %d\n", verse.TranslationAbbv, t.ID) 89 | } else { 90 | fmt.Printf("Translation %s found with ID %d\n", verse.TranslationAbbv, t.ID) 91 | } 92 | 93 | fmt.Printf("Checking if book %s exists in translation ID %d...\n", verse.Book, t.ID) 94 | err = db.Get(&b, `SELECT * FROM book WHERE translation_id = ? AND name = ?`, t.ID, verse.Book) 95 | if err != nil { 96 | fmt.Printf("Book %s does not exist, creating it...\n", verse.Book) 97 | bQuery := `INSERT INTO book (translation_id, name, abbreviation) VALUES (?, ?, ?) RETURNING id` 98 | err = db.QueryRow(bQuery, t.ID, verse.Book, verse.BookAbbv).Scan(&b.ID) 99 | if err != nil { 100 | return err 101 | } 102 | fmt.Printf("Created book %s with ID %d\n", verse.Book, b.ID) 103 | } else { 104 | fmt.Printf("Book %s found with ID %d\n", verse.Book, b.ID) 105 | } 106 | 107 | fmt.Printf("Checking if chapter %d exists in book ID %d...\n", verse.Chapter, b.ID) 108 | err = db.Get(&ch, `SELECT * FROM chapter WHERE book_id = ? AND number = ?`, b.ID, verse.Chapter) 109 | if err != nil { 110 | fmt.Printf("Chapter %d does not exist, creating it...\n", verse.Chapter) 111 | chQuery := `INSERT INTO chapter (book_id, number) VALUES (?, ?) RETURNING id` 112 | err = db.QueryRow(chQuery, b.ID, verse.Chapter).Scan(&ch.ID) 113 | if err != nil { 114 | return err 115 | } 116 | fmt.Printf("Created chapter %d with ID %d\n", verse.Chapter, ch.ID) 117 | } else { 118 | fmt.Printf("Chapter %d found with ID %d\n", verse.Chapter, ch.ID) 119 | } 120 | 121 | fmt.Printf("Inserting verse %d into chapter ID %d...\n", verse.Number, ch.ID) 122 | vQuery := `INSERT INTO verse (chapter_id, number, text, section) VALUES (?, ?, ?, ?)` 123 | _, err = db.Exec(vQuery, ch.ID, verse.Number, verse.Text, verse.Section) 124 | if err != nil { 125 | return err 126 | } 127 | fmt.Printf("Inserted verse %d into chapter ID %d\n", verse.Number, ch.ID) 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/Phillip-England/vbf v0.1.0 h1:wIJUpKGVqwaYpS4k3Ruq3TETVEkK9PISTaTx5TXkjXs= 4 | github.com/Phillip-England/vbf v0.1.0/go.mod h1:hqZAzPU2+uEvab2BEcCHiB0cMQaXBTyXKDOSeTqi3co= 5 | github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8= 6 | github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU= 7 | github.com/a-h/templ v0.2.771 h1:4KH5ykNigYGGpCe0fRJ7/hzwz72k3qFqIiiLLJskbSo= 8 | github.com/a-h/templ v0.2.771/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= 9 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 10 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 11 | github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= 12 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 13 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 14 | github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 15 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 16 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 17 | github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= 18 | github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 22 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 23 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 24 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 25 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 26 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 27 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 28 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 30 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 31 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 32 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 33 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 34 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 35 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 36 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 37 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 42 | github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 43 | github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= 44 | github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 45 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 46 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 49 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 50 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 51 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 52 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 53 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 54 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 55 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 56 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 57 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 58 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 60 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 61 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 62 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 63 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 64 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 65 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 66 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 67 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 68 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 73 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 74 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 75 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 76 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 77 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 85 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 86 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 87 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 88 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 89 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 90 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 91 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 92 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 93 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 94 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 95 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 96 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 97 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 98 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 99 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 100 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 101 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 102 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 103 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 104 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 105 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 106 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 107 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 108 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 109 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 110 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 111 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 114 | --------------------------------------------------------------------------------