commit 6c234a80844c91f198553018a8f5a99798ed3d31 Author: Neur0toxine Date: Sat Jun 8 22:16:40 2019 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be48bab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..301389f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Neur0toxine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e3838e --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +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 diff --git a/bashim.go b/bashim.go new file mode 100644 index 0000000..204fd33 --- /dev/null +++ b/bashim.go @@ -0,0 +1,123 @@ +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/main.go b/main.go new file mode 100644 index 0000000..f1f1bdf --- /dev/null +++ b/main.go @@ -0,0 +1,229 @@ +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) + } +}