mirror of
https://github.com/Neur0toxine/bash.im-telegram-bot.git
synced 2024-11-23 21:56:08 +03:00
Refactor
This commit is contained in:
parent
6c234a8084
commit
37d6d7baf7
7
.env.dist
Normal file
7
.env.dist
Normal file
@ -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.
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
*.exe
|
*.exe
|
||||||
.env
|
.env
|
||||||
|
bin/*
|
||||||
|
bin
|
23
Makefile
Normal file
23
Makefile
Normal file
@ -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)
|
@ -1,4 +1,7 @@
|
|||||||
This bot will show latest quotes from bash.im. Also it can (but not yet) work inline.
|
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
|
- [x] Ability to fetch latest quotes from bash.im
|
||||||
- [ ] Ability to send quote to dialog via inline mode
|
- [x] Ability to send quote to dialog via inline mode
|
||||||
- [ ] Integrated search and autocomplete for 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
|
123
bashim.go
123
bashim.go
@ -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]+?\<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
|
|
||||||
}
|
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -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
|
||||||
|
)
|
6
go.sum
Normal file
6
go.sum
Normal file
@ -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=
|
229
main.go
229
main.go
@ -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>, где 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)
|
|
||||||
}
|
|
||||||
}
|
|
130
src/bashim.go
Normal file
130
src/bashim.go
Normal file
@ -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]+?\<article\sclass\="quote\"[.\s\w\W]+?<\/article\>`)
|
||||||
|
getQuoteDataRe = 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+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
233
src/bot.go
Normal file
233
src/bot.go
Normal file
@ -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>`, где 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)
|
||||||
|
}
|
||||||
|
}
|
105
src/config.go
Normal file
105
src/config.go
Normal file
@ -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
|
||||||
|
}
|
35
src/main.go
Normal file
35
src/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user