diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..72d0876 --- /dev/null +++ b/.env.dist @@ -0,0 +1,7 @@ +TG_BOT_TOKEN=token # Telegram Bot API Token +POLL_TIMEOUT=30 # Poll timeout (in seconds). Default: 30 +WEBHOOK="https://www.google.com:{PORT}/{TOKEN}" # Webhook URL (don't provide if you want to use polling). {PORT} and {TOKEN} will be replaced automatically. +WEBHOOK_PORT=8000 # Webhook port. Ignored if webhook URL is not provided. Default: 8000 +CERT="cert.pem" # Certificate file name. Provide if you want to use SSL +CERT_KEY="key.pem" # Certificate key. Should be provided with CERT +DEBUG=false # Debug mode. This value will be passed to API library, which will log all requests in debug mode. \ No newline at end of file diff --git a/.gitignore b/.gitignore index be48bab..25cf76b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.exe -.env \ No newline at end of file +.env +bin/* +bin \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb27e08 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.DEFAULT_GOAL := build + +GO=$(shell which go) +PROJECT_DIR=$(shell pwd) +GOPATH=$(PROJECT_DIR) +SRC=$(PROJECT_DIR)/src +BIN=$(PROJECT_DIR)/bin/bash_im_bot + +build: fmt deps + @echo "- Building" + @cd $(SRC) && $(GO) build -o $(BIN) + @echo Built "$(BIN)" + +run: + @$(BIN) + +deps: + @echo "- Installing dependencies" + @$(GO) mod tidy + +fmt: + @echo "- Running 'go fmt'" + @$(GO) fmt $(SRC) \ No newline at end of file diff --git a/README.md b/README.md index 4e3838e..ae68932 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ This bot will show latest quotes from bash.im. Also it can (but not yet) work inline. - [x] Ability to fetch latest quotes from bash.im -- [ ] Ability to send quote to dialog via inline mode -- [ ] Integrated search and autocomplete for inline mode \ No newline at end of file +- [x] Ability to send quote to dialog via inline mode +- [x] Integrated search and autocomplete for inline mode +- [ ] Use [goquery](https://github.com/PuerkitoBio/goquery) instead of regular expressions +- [ ] Automated version increment +- [ ] Setup CI/CD \ No newline at end of file diff --git a/bashim.go b/bashim.go deleted file mode 100644 index 204fd33..0000000 --- a/bashim.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "errors" - "io/ioutil" - "net/http" - "regexp" - "strconv" - "strings" -) - -const BASH_URL = "https://bash.im" - -type BashQuote struct { - ID int - Created string - Rating string - Permalink string - Text string -} - -func GetBashQuote(id int) (BashQuote, error) { - var quote BashQuote - - if resp, err := http.Get(BASH_URL + "/quote/" + strconv.Itoa(id)); err == nil { - defer resp.Body.Close() - - if resp.StatusCode == 200 { - if bodyData, err := ioutil.ReadAll(resp.Body); err == nil { - body := string(bodyData) - items := getQuotesList(body) - - if len(items) == 1 { - return items[0], nil - } else { - return quote, errors.New("Can't find quote") - } - } else { - return quote, err - } - } else { - return quote, errors.New("Incorrect status code: " + strconv.Itoa(resp.StatusCode)) - } - } else { - return quote, err - } -} - -func GetLatestQuotes() ([]BashQuote, error) { - var quotes []BashQuote - - if resp, err := http.Get(BASH_URL); err == nil { - defer resp.Body.Close() - - if resp.StatusCode == 200 { - if bodyData, err := ioutil.ReadAll(resp.Body); err == nil { - body := string(bodyData) - items := getQuotesList(body) - - if len(items) > 1 { - return items, nil - } else { - return quotes, errors.New("Error while trying to extract quotes") - } - } else { - return quotes, err - } - } else { - return quotes, errors.New("Incorrect status code: " + strconv.Itoa(resp.StatusCode)) - } - } else { - return quotes, err - } -} - -func getQuotesList(response string) []BashQuote { - re := regexp.MustCompile(`(?im)[.\s\w\W]+?\`) - matches := re.FindAllString(response, -1) - items := make([]BashQuote, len(matches)) - - for index, match := range matches { - id, created, rating, permalink, text, err := getQuoteData(match) - - if err != nil { - continue - } - - items[index] = BashQuote{ - ID: id, - Created: created, - Rating: rating, - Permalink: permalink, - Text: text, - } - } - - return items -} - -func getQuoteData(response string) (id int, created string, rating string, permalink string, text string, err error) { - re := regexp.MustCompile(`(?im)data\-quote\=\"(?P\d+)\"[.\s\w\W]+?quote__header_permalink.+href\=\"(?P\/.+\d)\"[.\s\w\W]+?quote__header_date\"\>[.\s\w\W]+?(?P.+)[.\s\w\W]+?quote__body\"\>\s+?(?P.+)[.\s\w\W]+?quote__total.+\>(?P\d+)`) - matches := re.FindStringSubmatch(response) - - if len(matches) == 0 { - return 0, "", "", "", "", errors.New("No data found") - } else { - matches = matches[1:] - } - - id, err = strconv.Atoi(matches[0]) - - if err != nil { - return 0, "", "", "", "", err - } - - created = strings.TrimSpace(matches[2]) - rating = strings.TrimSpace(matches[4]) - permalink = BASH_URL + matches[1] - text = strings.ReplaceAll(strings.TrimSpace(matches[3]), "
", "\n") - err = nil - - return -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0526a16 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/Neur0toxine/bash.im-telegram-bot + +go 1.12 + +require ( + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible + github.com/joho/godotenv v1.3.0 + github.com/technoweenie/multipartstreamer v1.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25c0b99 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= diff --git a/main.go b/main.go deleted file mode 100644 index f1f1bdf..0000000 --- a/main.go +++ /dev/null @@ -1,229 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - "os" - "strconv" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/joho/godotenv" -) - -type updateFunc func(tgbotapi.Update, *tgbotapi.BotAPI) - -const envToken = "TG_BOT_TOKEN" -const envPollTimeout = "POLL_TIMEOUT" -const envWebhook = "WEBHOOK" -const envWebhookPort = "WEBHOOK_PORT" -const envCert = "CERT" -const envKey = "CERT_KEY" -const envDebug = "DEBUG" - -const defaultPollTimeout = 30 -const defaultWebhookPort = 8000 - -func main() { - var ( - pollTimeout, webhookPort int - err error - ) - - err = godotenv.Load() - - if err != nil { - log.Printf("WARNING: Error while loading `.env` file `%s`", err.Error()) - } - - token := os.Getenv(envToken) - webhookUrl := os.Getenv(envWebhook) - webhookPortStr := os.Getenv(envWebhookPort) - certFile := os.Getenv(envCert) - certKey := os.Getenv(envKey) - pollTimeoutStr := os.Getenv(envPollTimeout) - debug, err := strconv.ParseBool(os.Getenv(envDebug)) - - if err != nil { - debug = false - } - - if pollTimeout, err = strconv.Atoi(pollTimeoutStr); err != nil { - log.Printf("Using default poll timeout - %d seconds...", defaultPollTimeout) - pollTimeout = defaultPollTimeout - } - - if webhookPort, err = strconv.Atoi(webhookPortStr); err != nil && webhookUrl != "" { - log.Printf("Using default webhook port %d ...", defaultWebhookPort) - webhookPort = defaultWebhookPort - } - - if token == "" { - log.Fatalf( - "`%s` is not found in environment - specify it in `.env` file or pass it while launching", - envToken, - ) - } - - bot, err := tgbotapi.NewBotAPI(token) - - if err != nil { - log.Fatal(err) - } - - bot.Debug = debug - - log.Printf("Authorized on account: @%s (id: %d)", bot.Self.UserName, bot.Self.ID) - log.Printf("Debug mode: %t", debug) - - if webhookUrl == "" { - initWithPolling(bot, pollTimeout, processUpdate) - } else { - initWithWebhook(bot, webhookUrl, webhookPort, certFile, certKey, processUpdate) - } -} - -func processUpdate(update tgbotapi.Update, bot *tgbotapi.BotAPI) { - if update.InlineQuery != nil { - results := make([]interface{}, 1) - results[0] = tgbotapi.NewInlineQueryResultArticleMarkdown( - update.InlineQuery.ID, - "", - "", - ) - - bot.AnswerInlineQuery( - tgbotapi.InlineConfig{ - InlineQueryID: update.InlineQuery.ID, - Results: results, - CacheTime: 30, - IsPersonal: false, - NextOffset: "", - SwitchPMText: "", - SwitchPMParameter: "", - }, - ) - } - - msgs := make([]tgbotapi.MessageConfig, 1) - msgs[0] = tgbotapi.NewMessage(update.Message.Chat.ID, "") - msgs[0].ParseMode = "markdown" - - if update.Message.IsCommand() { - switch update.Message.Command() { - case "latest": - items, err := GetLatestQuotes() - - if err != nil { - msgs[len(msgs)-1].Text = "Не удалось получить последние цитаты :(" - } else { - for _, item := range items { - text := fmt.Sprintf( - "*Цитата:* [#%d](%s) \n"+ - "*Создано:* %s \n"+ - "*Рейтинг:* %s \n"+ - "*Текст:*\n %s \n\n", - item.ID, - item.Permalink, - item.Created, - item.Rating, - item.Text, - ) - - if len(msgs[len(msgs)-1].Text+text) > 4096 { - msgs = append(msgs, tgbotapi.NewMessage(update.Message.Chat.ID, text)) - msgs[len(msgs)-1].ParseMode = "markdown" - } else { - msgs[len(msgs)-1].Text += text - } - } - } - default: - msgs[len(msgs)-1].Text = "Как насчёт последних цитат? Используйте /latest" - } - } else { - text := fmt.Sprintf("Что вы пытаетесь здесь найти? Тут ничего нет...\n"+ - "Бот работает не так. Зайдите в любой чат, вызовите бота вот так: @%s , где ID - "+ - "это идентификатор цитаты на bash.im. И бот перешлёт её!", bot.Self.UserName) - msgs[len(msgs)-1] = tgbotapi.NewMessage(update.Message.Chat.ID, text) - msgs[len(msgs)-1].ReplyToMessageID = update.Message.MessageID - } - - for _, msg := range msgs { - if _, err := bot.Send(msg); err != nil { - log.Printf("Error while trying to send message to chat `%d`: %s", update.Message.Chat.ID, err.Error()) - } - } -} - -func initWithPolling(bot *tgbotapi.BotAPI, updateTimeout int, updateCallback updateFunc) { - var ( - updates tgbotapi.UpdatesChannel - err error - ) - - u := tgbotapi.NewUpdate(0) - u.Timeout = updateTimeout - - if updates, err = bot.GetUpdatesChan(u); err != nil { - log.Fatalf("Error while trying to get updates: %s", err.Error()) - } - - for update := range updates { - updateCallback(update, bot) - } -} - -func initWithWebhook( - bot *tgbotapi.BotAPI, - webhookUrl string, - webhookPort int, - certFile string, - certKey string, - updateCallback updateFunc, -) { - var err error - - if webhookPort == 0 { - webhookPort = 80 - } - - if webhookUrl == "" { - log.Fatalf("Empty webhook URL provided (env %s)", envWebhook) - } - - webhookLink := fmt.Sprintf("%s:%d/%s", webhookUrl, webhookPort, bot.Token) - - if certFile != "" && certKey != "" { - _, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert(webhookLink, "cert.pem")) - } else { - _, err = bot.SetWebhook(tgbotapi.NewWebhook(webhookLink)) - } - - if err != nil { - log.Fatal(err) - } - - info, err := bot.GetWebhookInfo() - - if err != nil { - log.Fatal(err) - } - - if info.LastErrorDate != 0 { - log.Printf("Telegram callback failed: %s", info.LastErrorMessage) - } - - updates := bot.ListenForWebhook("/" + bot.Token) - serverUrl := fmt.Sprintf("0.0.0.0:%d", webhookPort) - - if certFile != "" && certKey != "" { - go http.ListenAndServeTLS(serverUrl, certFile, certKey, nil) - } else { - go http.ListenAndServe(serverUrl, nil) - } - - for update := range updates { - updateCallback(update, bot) - } -} diff --git a/src/bashim.go b/src/bashim.go new file mode 100644 index 0000000..12f6fb3 --- /dev/null +++ b/src/bashim.go @@ -0,0 +1,130 @@ +package main + +import ( + "errors" + "fmt" + "html" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +const BASH_URL = "https://bash.im" + +type BashQuote struct { + ID int + Created string + Rating string + Permalink string + Text string +} + +var ( + replaceBrRe = regexp.MustCompile(`(?im)\<[\s+]?br[\s+\/]{0,2}?\>`) + getQuotesListRe = regexp.MustCompile(`(?im)[.\s\w\W]+?\`) + getQuoteDataRe = regexp.MustCompile(`(?im)data\-quote\=\"(?P\d+)\"[.\s\w\W]+?quote__header_permalink.+href\=\"(?P\/.+\d)\"[.\s\w\W]+?quote__header_date\"\>[.\s\w\W]+?(?P.+)[.\s\w\W]+?quote__body\"\>\s+?(?P.+)[.\s\w\W]+?quote__total.+\>(?P\d+)`) +) + +func getQuotesList(response string, maxItems int) []BashQuote { + var items []BashQuote + matches := getQuotesListRe.FindAllString(response, -1) + + if maxItems != 0 && len(matches) > maxItems { + matches = matches[:maxItems] + } + + for _, match := range matches { + id, created, rating, permalink, text, err := getQuoteData(match) + + if err != nil { + continue + } + + if id == 0 { + continue + } + + items = append(items, BashQuote{ + ID: id, + Created: created, + Rating: rating, + Permalink: permalink, + Text: text, + }) + } + + return items +} + +func getQuoteData(response string) (id int, created string, rating string, permalink string, text string, err error) { + matches := getQuoteDataRe.FindStringSubmatch(response) + + if len(matches) == 0 { + return 0, "", "", "", "", errors.New("No data found") + } else { + matches = matches[1:] + } + + id, err = strconv.Atoi(matches[0]) + + if err != nil { + return 0, "", "", "", "", err + } + + created = strings.ReplaceAll(strings.TrimSpace(matches[2]), " ", " ") + rating = strings.TrimSpace(matches[4]) + permalink = BASH_URL + matches[1] + text = html.UnescapeString(replaceBrRe.ReplaceAllString(strings.TrimSpace(matches[3]), "\n")) + err = nil + + return +} + +func GetLatestQuotes() ([]BashQuote, error) { + return extractQuotes("/", 25) +} + +func GetQuote(id int) (BashQuote, error) { + quotes, err := extractQuotes(fmt.Sprintf("/quote/%d", id), 1) + + if err != nil { + return BashQuote{}, err + } else if len(quotes) == 0 { + return BashQuote{}, errors.New("Error while trying to extract quote") + } else { + return quotes[0], nil + } +} + +func SearchQuotes(search string, maxResults int) ([]BashQuote, error) { + return extractQuotes(fmt.Sprintf("/search?text=%s", url.QueryEscape(search)), maxResults) +} + +func extractQuotes(url string, maxItems int) ([]BashQuote, error) { + var ( + quotes []BashQuote + link = fmt.Sprintf("%s%s", BASH_URL, url) + ) + + if resp, err := http.Get(link); err == nil { + defer resp.Body.Close() + + if resp.StatusCode == 200 { + if bodyData, err := ioutil.ReadAll(resp.Body); err == nil { + body := string(bodyData) + items := getQuotesList(body, maxItems) + + return items, nil + } else { + return quotes, err + } + } else { + return quotes, errors.New("Incorrect status code: " + strconv.Itoa(resp.StatusCode)) + } + } else { + return quotes, err + } +} diff --git a/src/bot.go b/src/bot.go new file mode 100644 index 0000000..b3dd7ef --- /dev/null +++ b/src/bot.go @@ -0,0 +1,233 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "net/http" + "strconv" + "unicode/utf16" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type updateFunc func(tgbotapi.Update, *tgbotapi.BotAPI) + +func processUpdate(update tgbotapi.Update, bot *tgbotapi.BotAPI) { + if update.InlineQuery != nil { + var ( + results []interface{} + bashQuotes []BashQuote + quote BashQuote + err error + ) + + if update.InlineQuery.Query == "" { + return + } + + if quoteId, errConv := strconv.Atoi(update.InlineQuery.Query); errConv == nil { + quote, err = GetQuote(quoteId) + bashQuotes = append(bashQuotes, quote) + } else { + log.Print(errConv) + bashQuotes, err = SearchQuotes(update.InlineQuery.Query, 3) + } + + if err == nil { + for _, quote := range bashQuotes { + utfEncodedString := utf16.Encode([]rune(quote.Text)) + runeString := utf16.Decode(utfEncodedString[:50]) + + title := fmt.Sprintf( + "[#%d]: %s\n", + quote.ID, + string(runeString)+"...\n", + ) + + text := fmt.Sprintf( + "*Цитата:* [#%d](%s), %s \n"+ + "*Рейтинг:* %s \n"+ + "%s \n\n", + quote.ID, + quote.Permalink, + quote.Created, + quote.Rating, + quote.Text, + ) + + results = append(results, tgbotapi.NewInlineQueryResultArticleMarkdown( + strconv.Itoa(rand.Int()), + title, + text, + )) + } + } else { + errMsg := "Не удалось произвести поиск" + results = append(results, tgbotapi.NewInlineQueryResultArticleMarkdown( + strconv.Itoa(rand.Int()), + errMsg, + errMsg, + )) + } + + if len(results) == 0 { + errMsg := "Ничего не найдено..." + results = append(results, tgbotapi.NewInlineQueryResultArticleMarkdown( + strconv.Itoa(rand.Int()), + errMsg, + errMsg, + )) + } + + response, err := bot.AnswerInlineQuery( + tgbotapi.InlineConfig{ + InlineQueryID: update.InlineQuery.ID, + Results: results, + CacheTime: 30, + IsPersonal: false, + NextOffset: "", + SwitchPMText: "", + SwitchPMParameter: "", + }, + ) + + if err != nil { + log.Print(err) + } + + if !response.Ok { + log.Printf("Error %d while trying to send inline update", response.ErrorCode) + } + } else { + msgs := make([]tgbotapi.MessageConfig, 1) + msgs[0] = NewMessage(update.Message.Chat.ID, 0, "", "markdown") + + if update.Message.IsCommand() { + switch update.Message.Command() { + case "latest": + SendMessages(bot, []tgbotapi.MessageConfig{ + NewMessage(update.Message.Chat.ID, update.Message.MessageID, "_Получаю свежие цитаты..._", "markdown"), + }) + + items, err := GetLatestQuotes() + + if err != nil { + msgs[len(msgs)-1].Text = "Не удалось получить последние цитаты :(" + } else { + for _, item := range items { + text := fmt.Sprintf( + "*Цитата:* [#%d](%s), %s \n"+ + "*Рейтинг:* %s \n"+ + "%s \n\n", + item.ID, + item.Permalink, + item.Created, + item.Rating, + item.Text, + ) + + if len(msgs[len(msgs)-1].Text+text) > 4096 { + msgs = append(msgs, NewMessage(update.Message.Chat.ID, 0, text, "markdown")) + } else { + msgs[len(msgs)-1].Text += text + } + } + } + default: + msgs[len(msgs)-1].Text = "Как насчёт последних цитат? Используйте /latest" + } + } else { + text := fmt.Sprintf("Зайдите в любой чат, вызовите бота вот так:\n `@%s `, где ID - "+ + "это идентификатор цитаты на bash.im. И бот перешлёт её!\n"+ + "Ещё вместо идентификатора можно указать текст, по которому бот попытается найти цитаты.", bot.Self.UserName) + msgs[len(msgs)-1] = NewMessage(update.Message.Chat.ID, update.Message.MessageID, text, "markdown") + } + + SendMessages(bot, msgs) + } +} + +func NewMessage(chatID int64, replyTo int, text string, parse string) tgbotapi.MessageConfig { + msg := tgbotapi.NewMessage(chatID, text) + msg.ParseMode = parse + + if replyTo != 0 { + msg.ReplyToMessageID = replyTo + } + + return msg +} + +func SendMessages(bot *tgbotapi.BotAPI, msgs []tgbotapi.MessageConfig) { + for _, msg := range msgs { + if _, err := bot.Send(msg); err != nil { + log.Printf("Error while trying to send message to chat `%d`: %s", msg.BaseChat.ChatID, err.Error()) + } + } +} + +func initWithPolling(bot *tgbotapi.BotAPI, updateTimeout int, updateCallback updateFunc) { + var ( + updates tgbotapi.UpdatesChannel + err error + ) + + u := tgbotapi.NewUpdate(0) + u.Timeout = updateTimeout + + if updates, err = bot.GetUpdatesChan(u); err != nil { + log.Fatalf("Error while trying to get updates: %s", err.Error()) + } + + for update := range updates { + go updateCallback(update, bot) + } +} + +func initWithWebhook( + bot *tgbotapi.BotAPI, + webhookUrl string, + listenAddr string, + certFile string, + certKey string, + updateCallback updateFunc, +) { + var err error + + if webhookUrl == "" { + log.Fatalf("Empty webhook URL provided (env %s)", envWebhook) + } + + if certFile != "" && certKey != "" { + _, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert(webhookUrl, certFile)) + } else { + _, err = bot.SetWebhook(tgbotapi.NewWebhook(webhookUrl)) + } + + if err != nil { + log.Fatal(err) + } + + info, err := bot.GetWebhookInfo() + + if err != nil { + log.Fatal(err) + } + + if info.LastErrorDate != 0 { + log.Printf("Telegram callback failed: %s", info.LastErrorMessage) + } + + updates := bot.ListenForWebhook("/" + bot.Token) + + if certFile != "" && certKey != "" { + go http.ListenAndServeTLS(listenAddr, certFile, certKey, nil) + } else { + go http.ListenAndServe(listenAddr, nil) + } + + for update := range updates { + go updateCallback(update, bot) + } +} diff --git a/src/config.go b/src/config.go new file mode 100644 index 0000000..c2823e6 --- /dev/null +++ b/src/config.go @@ -0,0 +1,105 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/joho/godotenv" +) + +const envToken = "TG_BOT_TOKEN" +const envPollTimeout = "POLL_TIMEOUT" +const envListen = "LISTEN_IP" +const envWebhook = "WEBHOOK" +const envWebhookPort = "WEBHOOK_PORT" +const envCert = "CERT" +const envKey = "CERT_KEY" +const envDebug = "DEBUG" + +const defaultPollTimeout = 30 +const defaultWebhookPort = 8000 + +const ModePolling = "polling" +const ModeWebhook = "webhook" + +type BotConfig struct { + Token string + Mode string + Debug bool + PollingTimeout int + WebhookURL string + ListenAddr string + CertificateFile string + CertificateKey string +} + +func LoadConfig() (BotConfig, error) { + var ( + cfg BotConfig + pollTimeout, webhookPort int + ) + + if err := godotenv.Load(); err != nil { + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + + if err != nil { + log.Fatal(err) + } + + if err := godotenv.Load(filepath.Join(filepath.Dir(dir), ".env")); err != nil { + return cfg, err + } + } + + token := os.Getenv(envToken) + webhookUrl := os.Getenv(envWebhook) + listenAddr := os.Getenv(envListen) + webhookPortStr := os.Getenv(envWebhookPort) + certFile := os.Getenv(envCert) + certKey := os.Getenv(envKey) + pollTimeoutStr := os.Getenv(envPollTimeout) + debug, err := strconv.ParseBool(os.Getenv(envDebug)) + + if err != nil { + debug = false + } + + if pollTimeout, err = strconv.Atoi(pollTimeoutStr); err != nil { + pollTimeout = defaultPollTimeout + } + + if webhookPort, err = strconv.Atoi(webhookPortStr); err != nil && webhookUrl != "" { + webhookPort = defaultWebhookPort + } + + webhookLink := strings.ReplaceAll(webhookUrl, "{PORT}", strconv.Itoa(webhookPort)) + webhookLink = strings.ReplaceAll(webhookLink, "{TOKEN}", token) + + if token == "" { + log.Fatalf( + "`%s` is not found in environment - specify it in `.env` file or pass it while launching", + envToken, + ) + } + + cfg = BotConfig{ + Token: token, + Debug: debug, + PollingTimeout: pollTimeout, + WebhookURL: webhookLink, + ListenAddr: listenAddr, + CertificateFile: certFile, + CertificateKey: certKey, + } + + if webhookUrl == "" { + cfg.Mode = ModePolling + } else { + cfg.Mode = ModeWebhook + } + + return cfg, nil +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..f1675df --- /dev/null +++ b/src/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "math/rand" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +func main() { + cfg, err := LoadConfig() + + if err != nil { + log.Printf("WARNING: Error while loading `.env` file `%s`", err.Error()) + } + + bot, err := tgbotapi.NewBotAPI(cfg.Token) + + if err != nil { + log.Fatal(err) + } + + bot.Debug = cfg.Debug + + log.Printf("Authorized on account: @%s (id: %d)", bot.Self.UserName, bot.Self.ID) + log.Printf("Debug mode: %t", cfg.Debug) + rand.Seed(time.Now().UTC().UnixNano()) + + if cfg.WebhookURL == "" { + initWithPolling(bot, cfg.PollingTimeout, processUpdate) + } else { + initWithWebhook(bot, cfg.WebhookURL, cfg.ListenAddr, cfg.CertificateFile, cfg.CertificateKey, processUpdate) + } +}