Initial commit

This commit is contained in:
Pavel 2019-06-08 22:16:40 +03:00
commit 6c234a8084
5 changed files with 379 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.exe
.env

21
LICENSE Normal file
View File

@ -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.

4
README.md Normal file
View File

@ -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

123
bashim.go Normal file
View File

@ -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]+?\<article\sclass\="quote\"[.\s\w\W]+?<\/article\>`)
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<id>\d+)\"[.\s\w\W]+?quote__header_permalink.+href\=\"(?P<permalink>\/.+\d)\"[.\s\w\W]+?quote__header_date\"\>[.\s\w\W]+?(?P<date>.+)[.\s\w\W]+?quote__body\"\>\s+?(?P<text>.+)[.\s\w\W]+?quote__total.+\>(?P<rating>\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]), "<br>", "\n")
err = nil
return
}

229
main.go Normal file
View File

@ -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>, где 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)
}
}