diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..22d924f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +bible_databases + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2f11784 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.16-buster AS build + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ + +ENV CGO_ENABLED 0 + +RUN go mod download + +COPY *.go ./ + +RUN go build -o /biblebot *.go + +## +## Deploy +## +FROM alpine + +WORKDIR / +COPY --from=build /biblebot /biblebot + +ENTRYPOINT ["/biblebot"] diff --git a/README.md b/README.md index c684e84..0b06034 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,15 @@ git submodule update ``` Ensure Docker and docker-compose are installed +Create a file called `.env` and add the following to it: +``` +RDRAMA_API_TOKEN= +RDRAMA_URL=https://rdrama.net +MARIADB_RANDOM_ROOT_PASSWORD=yes +MARIADB_USER=admin +MARIADB_PASSWORD=mysafeadminpassword +MARIADB_DATABASE=bible +BIBLEDB_HOST=bibledb +BIBLEBOT_ID= +``` +Fill in your `RDRAMA_API_TOKEN` and `BIBLEBOT_ID`. You can change the user and password. diff --git a/bible.go b/bible.go index b69ec7b..3e4c0a0 100644 --- a/bible.go +++ b/bible.go @@ -11,6 +11,75 @@ var BibleRegex = regexp.MustCompile(`(?P\w*?)? *(?P[a-zA-Z]+?)\s?( var MatchError = errors.New("Error matching verse") +var CanonicalName = map[int]string{ + 1: "Genesis", + 2: "Exodus", + 3: "Leviticus", + 4: "Numbers", + 5: "Deuteronomy", + 6: "Joshua", + 7: "Judges", + 8: "Ruth", + 9: "1 Samuel", + 10: "2 Samuel", + 11: "1 Kings", + 12: "2 Kings", + 13: "1 Chronicles", + 14: "2 Chronicles", + 15: "Ezra", + 16: "Nehemiah", + 17: "Esther", + 18: "Job", + 19: "Psalms", + 20: "Proverbs", + 21: "Ecclesiastes", + 22: "Song of Solomon", + 23: "Isaiah", + 24: "Jeremiah", + 25: "Lamentations", + 26: "Ezekiel", + 27: "Daniel", + 28: "Hosea", + 29: "Joel", + 30: "Amos", + 31: "Obadiah", + 32: "Jonah", + 33: "Micah", + 34: "Nahum", + 35: "Habakkuk", + 36: "Zephaniah", + 37: "Haggai", + 38: "Zechariah", + 39: "Malachi", + 40: "Matthew", + 41: "Mark", + 42: "Luke", + 43: "John", + 44: "Acts", + 45: "Romans", + 46: "1 Corinthians", + 47: "2 Corinthians", + 48: "Galatians", + 49: "Ephesians", + 50: "Philippians", + 51: "Colossians", + 52: "1 Thessalonians", + 53: "2 Thessalonians", + 54: "1 Timothy", + 55: "2 Timothy", + 56: "Titus", + 57: "Philemon", + 58: "Hebrews", + 59: "James", + 60: "1 Peter", + 61: "2 Peter", + 62: "1 John", + 63: "2 John", + 64: "3 John", + 65: "Jude", + 66: "Revelation", +} + var Books = map[string]int{ "GEN": 1, "EXO": 2, @@ -158,6 +227,17 @@ func contains(s []string, str string) bool { return false } +func (b *BibleReference) CanonicalName() (string, error) { + book, ok := CanonicalName[b.BookNum] + if !ok { + return "", errors.New("Can't find book, maybe you're Catholic?") + } + if b.StartKey == b.EndKey { + return fmt.Sprintf("%s %s:%s", book, b.Chapter, b.StartVerse), nil + } + return fmt.Sprintf("%s %s:%s-%s", book, b.Chapter, b.StartVerse, b.EndVerse), nil +} + func (b *BibleReference) Validate() error { if contains(FirstPrefixes, b.Prefix) { b.Prefix = "1" @@ -179,6 +259,7 @@ func (b *BibleReference) Validate() error { if !ok { return MatchError } + b.BookNum = num chapter, err := strconv.Atoi(b.Chapter) if err != nil { diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..47ab5d7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,31 @@ +version: '3' + +# +# Docker Compose configuration for MariaDB +# +# To start the MariaDB instance and import the Bible data: docker-compose up +# +# To connect to the database from the host (presuming you have mysql-client or +# mariadb-client installed), use a command like this: +# mysql -h 127.0.0.1 -u root -p bible +# +# More information about Docker Compose: https://docs.docker.com/compose/ +# + +services: + bibledb: + image: mariadb:10.5 + volumes: + - ./bible_databases/sql:/docker-entrypoint-initdb.d + env_file: ['.env'] + restart: always + + biblebot: + build: + context: . + env_file: ['.env'] + depends_on: + - bibledb + volumes: + - ./lastrun.txt:/lastrun.txt +# restart: unless-stopped diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f5910e --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module fsdfsd.net/GeneralHurricane/BibleBot + +go 1.15 + +require github.com/go-sql-driver/mysql v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7109e4c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= diff --git a/main.go b/main.go index 00a18c2..c9b3f96 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,39 @@ package main import ( + "bufio" + "database/sql" "fmt" "log" "os" + "strconv" "strings" "time" + + _ "github.com/go-sql-driver/mysql" ) +const NAME string = "Tʜᴇ Lᴏʀᴅ" + +var BibleDB *sql.DB +var BibleQuery *sql.Stmt +var Client *RDramaClient +var WAIT_TIME time.Duration +var ONE_SEC time.Duration +var BibleBotID int + func main() { + ERRCOUNT := 0 + WAIT_TIME, err := time.ParseDuration("1m") + if err != nil { + log.Fatal("Error parsing time") + } + time.Sleep(WAIT_TIME) + ONE_SEC, err = time.ParseDuration("1s") + if err != nil { + log.Fatal("Error parsing time 2") + } + fmt.Printf("Running...\n") siteURL := os.Getenv("RDRAMA_URL") if siteURL == "" { @@ -20,24 +45,191 @@ func main() { log.Fatal("RDRAMA_API_TOKEN environment variable not set") } - client := NewRDramaClient(siteURL, siteToken) - now := time.Now() - - comments, err := client.GetComments(SearchParams{ - Hole: "dankchristianmemes", - After: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC), - }) - + botId := os.Getenv("BIBLEBOT_ID") + if botId == "" { + log.Fatal("BIBLEBOT_ID environment variable not set") + } + BibleBotID, err = strconv.Atoi(botId) if err != nil { - log.Fatal(err) - panic(err) - } - fmt.Printf("Found comments: %d\n", len(comments)) - - for _, comment := range comments { -// fmt.Printf("%s: %s\n", comment.Author, comment.Text) - _ = GetReferences(strings.ToUpper(comment.Text)) + log.Fatal("Error loading BIBLEBOT_ID: ", botId) } - // Use the client to make API requests + Client = NewRDramaClient(siteURL, siteToken) + + username := os.Getenv("MARIADB_USER") + password := os.Getenv("MARIADB_PASSWORD") + host := os.Getenv("BIBLEDB_HOST") + database := os.Getenv("MARIADB_DATABASE") + + for ERRCOUNT < 5 { + BibleDB, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", username, password, host, database)) + if err != nil { + fmt.Println("ERROR connecting: ", err) + fmt.Println("DB string: ", fmt.Sprintf("%s:%s@tcp(%s)/%s", username, password, host, database)) + ERRCOUNT = ERRCOUNT + 1 + time.Sleep(WAIT_TIME) + } else { + ERRCOUNT = 0 + break + } + } + + defer BibleDB.Close() + BibleQuery, err = BibleDB.Prepare("SELECT t FROM t_web WHERE id BETWEEN ? AND ?") + if err != nil { + fmt.Println(err) + log.Fatal("Unable to prepare query") + } + defer BibleQuery.Close() + + now := time.Now() + lastrun := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()-1, 0, 0, 0, time.UTC) + file, err := os.Open("lastrun.txt") + if err == nil { + + // Create a new scanner to read the file + scanner := bufio.NewScanner(file) + + // Read the first line of the file + scanner.Scan() + line := scanner.Text() + // Convert the line to an integer + num, err := strconv.ParseInt(line, 10, 64) + if err == nil { + + newTime := time.Unix(num, 0) + // fmt.Println("New time: ", newTime) + if newTime != time.Unix(0, 0) { + lastrun = newTime + } + } else { + fmt.Println("error parsing time", err) + } + file.Close() + } else { + fmt.Println("Err opening lastrun: ", err) + } + + for ERRCOUNT < 5 { + fmt.Println("\n\n-------------------------------------------------------") + fmt.Printf("Posts between %v - %v\n", lastrun, now) + posts, err := Client.GetPosts(SearchParams{ + Hole: "dankchristianmemes", + After: lastrun, + Before: now, + }) + + if err == nil { + + fmt.Printf("Found posts: %d\n", len(posts)) + + for _, post := range posts { + // fmt.Printf("%s: %s\n", comment.AuthorName, comment.Text) + if post.Author.ID != BibleBotID { // originally forgot this lol + handleAndReply(fmt.Sprintf("%s\n%s", post.Title, post.Text), fmt.Sprintf("p_%v", post.ID)) + } else { + fmt.Println("Skipping own post") + } + } + } else { + ERRCOUNT = ERRCOUNT + 1 + time.Sleep(ONE_SEC) + continue + + } + fmt.Printf("Comments between %v - %v\n", lastrun, now) + comments, err := Client.GetComments(SearchParams{ + Hole: "dankchristianmemes", + After: lastrun, + Before: now, + }) + + if err == nil { + + fmt.Printf("Found comments: %d\n", len(comments)) + + for _, comment := range comments { + // fmt.Printf("%s: %s\n", comment.AuthorName, comment.Text) + if comment.Author.ID != BibleBotID { + handleAndReply(comment.Text, fmt.Sprintf("c_%v", comment.ID)) + } else { + fmt.Println("Skipping own comment") + } + // Client.UpvoteComment(comment.ID) // not allowed for bots + + } + lastrun = now.Add(ONE_SEC) + file, err = os.Create("lastrun.txt") + if err == nil { + _, err = file.WriteString(strconv.FormatInt(lastrun.Unix(), 10)) + if err != nil { + fmt.Println("FILE ERROR: ", err) + } + err = file.Sync() + if err != nil { + fmt.Println("FILE ERROR: ", err) + } + + file.Close() + } + if err != nil { + fmt.Println("FILE ERROR: ", err) + } + + } else { + ERRCOUNT = ERRCOUNT + 1 + if ERRCOUNT > 4 { + log.Fatal(err) + } + } + time.Sleep(WAIT_TIME) + now = time.Now() + } +} + +func handleAndReply(text string, parent_fullname string) { + + refs := GetReferences(strings.ToUpper(text)) + if len(refs) == 0 { + return + } + var post strings.Builder + + for _, ref := range refs { + rows, err := BibleQuery.Query(ref.StartKey, ref.EndKey) + if err != nil { + fmt.Println(err) + continue + } + canName, err := ref.CanonicalName() + post.WriteString("**") + if err != nil { + post.WriteString(ref.RegexMatch) + } else { + post.WriteString(canName) + } + post.WriteString("**\n> ") + for rows.Next() { + var verse string + err = rows.Scan(&verse) + if err != nil { + fmt.Println(err) + continue + } + verse = strings.ReplaceAll(verse, "Yahweh", NAME) + verse = strings.ReplaceAll(verse, "\n", "\n> ") + post.WriteString(verse) + post.WriteString(" ") + } + rows.Close() + post.WriteString("\n\n") + } + fmt.Println(post.String()) + + err := Client.PostComment(post.String(), parent_fullname) + if err != nil { + fmt.Println(err) + } else { + time.Sleep(ONE_SEC) + } } diff --git a/rdrama.go b/rdrama.go index c88eace..4c12e7e 100644 --- a/rdrama.go +++ b/rdrama.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "encoding/json" "fmt" + "io" "net/http" - "net/url" + neturl "net/url" "time" ) @@ -20,15 +22,20 @@ func NewRDramaClient(baseURL, apiKey string) *RDramaClient { } } -func (c *RDramaClient) makeRequest(method, path string, v interface{}) error { +func (c *RDramaClient) makeRequest(method, path string, body string, response interface{}) error { url := c.baseURL + path - req, err := http.NewRequest(method, url, nil) + var reader io.Reader + + reader = bytes.NewBufferString(body) + + req, err := http.NewRequest(method, url, reader) if err != nil { return err } - req.Header.Add("Authorization", fmt.Sprintf("%s", c.apiKey)) - + if body != "" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } client := http.Client{} resp, err := client.Do(req) if err != nil { @@ -37,28 +44,71 @@ func (c *RDramaClient) makeRequest(method, path string, v interface{}) error { defer resp.Body.Close() if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + fmt.Println("request:\n", req) return fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - return json.NewDecoder(resp.Body).Decode(v) + if response != nil { + return json.NewDecoder(resp.Body).Decode(response) + } + // fmt.Println("reponse: ") + // _, err = io.Copy(os.Stdout, resp.Body) + if err != nil { + fmt.Println("Error copyng response body: ", err) + // handle the error + } + return nil } type CommentResponse struct { Data []Comment `json:"data"` } +type PostResponse struct { + Data []Post `json:"data"` +} + type Comment struct { ID int `json:"id"` - Author string `json:"author_name"` + Author Author `json:"author"` + AuthorName string `json:"author_name"` Text string `json:"body"` } +type Author struct { + ID int `json:id` +} + +type Post struct { + ID int `json:"id"` + Author Author `json:"author"` + AuthorName string `json:"author_name"` + Text string `json:"body"` + Title string `json:"title"` +} + +type CommentPost struct { + ParentID string `json:parent_fullname` + Body string `json:body` +} + type SearchParams struct { Hole string After time.Time Before time.Time } +func (c *RDramaClient) UpvotePost(id int) error { + url := fmt.Sprintf("/vote/post/%v/1", id) + return c.makeRequest("POST", url, "", nil) + +} + +func (c *RDramaClient) UpvoteComment(id int) error { + url := fmt.Sprintf("/vote/comment/%v/1", id) + return c.makeRequest("POST", url, "", nil) + +} func (c *RDramaClient) GetComments(params SearchParams) ([]Comment, error) { var s string if params.Hole != "" { @@ -71,14 +121,47 @@ func (c *RDramaClient) GetComments(params SearchParams) ([]Comment, error) { if params.Before != nullTime { s += fmt.Sprintf(" before:%d", params.Before.Unix()) } - v := url.Values{} + v := neturl.Values{} v.Set("q", s) url := "/search/comments?" + v.Encode() fmt.Printf("URL: %s\n", url) var comments CommentResponse - if err := c.makeRequest("GET", url, &comments); err != nil { + if err := c.makeRequest("GET", url, "", &comments); err != nil { return nil, err } return comments.Data, nil } + +func (c *RDramaClient) GetPosts(params SearchParams) ([]Post, error) { + var s string + if params.Hole != "" { + s += fmt.Sprintf("hole:%s", params.Hole) + } + nullTime := time.Time{} + if params.After != nullTime { + s += fmt.Sprintf(" after:%d", params.After.Unix()) + } + if params.Before != nullTime { + s += fmt.Sprintf(" before:%d", params.Before.Unix()) + } + v := neturl.Values{} + v.Set("q", s) + url := "/search/posts?" + v.Encode() + fmt.Printf("URL: %s\n", url) + var posts PostResponse + if err := c.makeRequest("GET", url, "", &posts); err != nil { + return nil, err + } + + return posts.Data, nil +} + +func (c *RDramaClient) PostComment(body string, parent string) error { + v := neturl.Values{} + v.Set("parent_fullname", parent) + v.Set("body", body) + fmt.Println(v.Encode()) + return c.makeRequest("POST", "/comment", v.Encode(), nil) + +}