mirror of
https://github.com/Neur0toxine/bash.im-telegram-bot.git
synced 2024-11-21 20:56:10 +03:00
Initial commit
This commit is contained in:
commit
6c234a8084
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.exe
|
||||||
|
.env
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
4
README.md
Normal 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
123
bashim.go
Normal 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
229
main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user