- Создаем бота
- Пишем код
- Разворачиваем бота
- Заключение
Сейчас очень популярен телеграм и написание ботов для него стало неким hello world наших дней, ввиду чего при мысли о том что можно написать сейчас, многие сразу же думают о написании телеграм бота.
Будучи студентом, я как и все студенты очень часто посещаю википедию параллельно уделяя время и телеграмму. Было решено найти способ совместить нахождение в телеграмме и возможность найти нужный мне материал в википедии, собственно так и появился этот бот(на момент написания бота я даже не подозревал о том что есть бот для этой же цели от команды телеграма).
Создаем бота
Идем к BotFather и создаем нового бота.
Далее добавляем ему команду с помощью которой мы сможем получить количество пользователей которые использовали бота.
Пишем код
Так как наш бот будет жить внутри докер контейнера то сразу будем устанавливать переменные окружения чтобы задавать параметры не входя в контейнер.
Из нестандартных библиотек нам понадобятся.
go get github.com/Syfaro/telegram-bot-api
Для работы с TelegramAPI.
go get github.com/lib/pq
Для работы с БД Postgres.
Создадим структуры.
type SearchResults struct {
ready bool
Query string
Results []Result
}
type Result struct {
Name, Description, URL string
}
Обратите внимание на поле ready bool, если структура пустая то значение будет false.
Далее заносим данные в структуры.
func (sr *SearchResults) UnmarshalJSON(bs []byte) error {
array := []interface{}{}
if err := json.Unmarshal(bs, &array); err != nil {
return err
}
sr.Query = array[0].(string)
for i := range array[1].([]interface{}) {
sr.Results = append(sr.Results, Result{
array[1].([]interface{})[i].(string),
array[2].([]interface{})[i].(string),
array[3].([]interface{})[i].(string),
})
}
return nil
}
Теперь нам нужна функция которая будет отправлять и получать данные.
func wikipediaAPI(request string) (answer []string) {
//Создаем срез на 3 элемента
s := make([]string, 3)
//Отправляем запрос
if response, err := http.Get(request); err != nil {
s[0] = "Wikipedia is not respond"
} else {
defer response.Body.Close()
//Считываем ответ
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
//Отправляем данные в структуру
sr := &SearchResults{}
if err = json.Unmarshal([]byte(contents), sr); err != nil {
s[0] = "Something going wrong, try to change your question"
}
//Проверяем не пустая ли наша структура
if !sr.ready {
s[0] = "Something going wrong, try to change your question"
}
//Проходим через нашу структуру и отправляем данные в срез с ответом
for i := range sr.Results {
s[i] = sr.Results[i].URL
}
}
return s
}
type SearchResults struct {
ready bool
Query string
Results []Result
}
type Result struct {
Name, Description, URL string
}
func (sr *SearchResults) UnmarshalJSON(bs []byte) error {
array := []interface{}{}
if err := json.Unmarshal(bs, &array); err != nil {
return err
}
sr.Query = array[0].(string)
for i := range array[1].([]interface{}) {
sr.Results = append(sr.Results, Result{
array[1].([]interface{})[i].(string),
array[2].([]interface{})[i].(string),
array[3].([]interface{})[i].(string),
})
}
return nil
}
func wikipediaAPI(request string) (answer []string) {
//Создаем срез на 3 элемента
s := make([]string, 3)
//Отправляем запрос
if response, err := http.Get(request); err != nil {
s[0] = "Wikipedia is not respond"
} else {
defer response.Body.Close()
//Считываем ответ
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
//Отправляем данные в структуру
sr := &SearchResults{}
if err = json.Unmarshal([]byte(contents), sr); err != nil {
s[0] = "Something going wrong, try to change your question"
}
//Проверяем не пустая ли наша структура
if !sr.ready {
s[0] = "Something going wrong, try to change your question"
}
//Проходим через нашу структуру и отправляем данные в срез с ответом
for i := range sr.Results {
s[i] = sr.Results[i].URL
}
}
return s
}
Ввиду того что мы отправляем URL нам нужно конвертировать сообщение от пользователя в часть URL. Зачем это нужно, затем что пользователь может отправить боту не одно а два слова через пробел, нам же нужно заменить пробел так чтобы он стал частью URL. Этим займется функция urlEncoded.
//Конвертируем запрос для использование в качестве части URL
func urlEncoded(str string) (string, error) {
u, err := url.Parse(str)
if err != nil {
return "", err
}
return u.String(), nil
}
Теперь нам нужно как то взаимодействовать с БД. Создаем переменные в которых мы будем хранить данные переменных окружения для подключению к БД.
var host = os.Getenv("HOST")
var port = os.Getenv("PORT")
var user = os.Getenv("USER")
var password = os.Getenv("PASSWORD")
var dbname = os.Getenv("DBNAME")
var sslmode = os.Getenv("SSLMODE")
var dbInfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbname, sslmode)
И пишем функцию которая будет создавать таблицу в нашей БД.
//Создаем таблицу users в БД при подключении к ней
func createTable() error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Создаем таблицу users
if _, err = db.Exec(`CREATE TABLE users(ID SERIAL PRIMARY KEY, TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, USERNAME TEXT, CHAT_ID INT, MESSAGE TEXT, ANSWER TEXT);`); err != nil {
return err
}
return nil
}
Таблицу мы создали, и нам нужно заносить в нее данные, этим займется следующая функция.
//Собираем данные полученные ботом
func collectData(username string, chatid int64, message string, answer []string) error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Конвертируем срез с ответом в строку
answ := strings.Join(answer, ", ")
//Создаем SQL запрос
data := `INSERT INTO users(username, chat_id, message, answer) VALUES($1, $2, $3, $4);`
//Выполняем наш SQL запрос
if _, err = db.Exec(data, `@`+username, chatid, message, answ); err != nil {
return err
}
return nil
}
Также давайте напишем функцию которая будет считать количество уникальных пользователей которые писали боту, чтобы отдавать это число пользователям если они отправят боту нужную команду.
func getNumberOfUsers() (int64, error) {
var count int64
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return 0, err
}
defer db.Close()
//Отправляем запрос в БД для подсчета числа уникальных пользователей
row := db.QueryRow("SELECT COUNT(DISTINCT username) FROM users;")
err = row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
var host = os.Getenv("HOST")
var port = os.Getenv("PORT")
var user = os.Getenv("USER")
var password = os.Getenv("PASSWORD")
var dbname = os.Getenv("DBNAME")
var sslmode = os.Getenv("SSLMODE")
var dbInfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbname, sslmode)
//Собираем данные полученные ботом
func collectData(username string, chatid int64, message string, answer []string) error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Конвертируем срез с ответом в строку
answ := strings.Join(answer, ", ")
//Создаем SQL запрос
data := `INSERT INTO users(username, chat_id, message, answer) VALUES($1, $2, $3, $4);`
//Выполняем наш SQL запрос
if _, err = db.Exec(data, `@`+username, chatid, message, answ); err != nil {
return err
}
return nil
}
//Создаем таблицу users в БД при подключении к ней
func createTable() error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Создаем таблицу users
if _, err = db.Exec(`CREATE TABLE users(ID SERIAL PRIMARY KEY, TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, USERNAME TEXT, CHAT_ID INT, MESSAGE TEXT, ANSWER TEXT);`); err != nil {
return err
}
return nil
}
func getNumberOfUsers() (int64, error) {
var count int64
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return 0, err
}
defer db.Close()
//Отправляем запрос в БД для подсчета числа уникальных пользователей
row := db.QueryRow("SELECT COUNT(DISTINCT username) FROM users;")
err = row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
Соединяем все воедино
Создаем бота.
//Создаем бота
bot, err := tgbotapi.NewBotAPI(os.Getenv("TOKEN"))
if err != nil {
panic(err)
}
//Устанавливаем время обновления
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
//Получаем обновления от бота
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
Нам нужно проверить что от пользователя приходит именно текстовое сообщение этим займется наш if.
if reflect.TypeOf(update.Message.Text).Kind() == reflect.String && update.Message.Text != ""
В случае если пользователь отправил стикер или же видео то отправим пользователю сообщение которое укажет ему на то что подходят только текстовые сообщения.
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Use the words for search.")
bot.Send(msg)
Внутрь if мы вложим switch который будет ловить команды:
/start
case "/start":
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Hi, i'm a wikipedia bot, i can search information in a wikipedia, send me something what you want find in Wikipedia.")
bot.Send(msg)
/number_of_users
case "/number_of_users":
if os.Getenv("DB_SWITCH") == "on" {
//Присваиваем количество пользоватьелей использовавших бота в num переменную
num, err := getNumberOfUsers()
if err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error.")
bot.Send(msg)
}
//Создаем строку которая содержит колличество пользователей использовавших бота
ans := fmt.Sprintf("%d peoples used me for search information in Wikipedia", num)
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, ans)
bot.Send(msg)
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database not connected, so i can't say you how many peoples used me.")
bot.Send(msg)
}
и default который будет взаимодействовать с википедией и бд
default:
//Устанавливаем язык для поиска в Википедии
language := os.Getenv("LANGUAGE")
//Создаем url для поиска
ms, _ := urlEncoded(update.Message.Text)
url := ms
request := "https://" + language + ".wikipedia.org/w/api.php?action=opensearch&search=" + url + "&limit=3&origin=*&format=json"
//Присваем данные среза с ответом в переменную message
message := wikipediaAPI(request)
if os.Getenv("DB_SWITCH") == "on" {
//Отправляем username, chat_id, message, answer в БД
if err := collectData(update.Message.Chat.UserName, update.Message.Chat.ID, update.Message.Text, message); err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error, but bot still working.")
bot.Send(msg)
}
}
//Проходим через срез и отправляем каждый элемент пользователю
for _, val := range message {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, val)
bot.Send(msg)
}
}
Создаем main.
func main() {
time.Sleep(1 * time.Minute)
//Создаем таблицу
if os.Getenv("CREATE_TABLE") == "yes" {
if os.Getenv("DB_SWITCH") == "on" {
if err := createTable(); err != nil {
panic(err)
}
}
}
time.Sleep(1 * time.Minute)
//Вызываем бота
telegramBot()
}
func telegramBot() {
//Создаем бота
bot, err := tgbotapi.NewBotAPI(os.Getenv("TOKEN"))
if err != nil {
panic(err)
}
//Устанавливаем время обновления
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
//Получаем обновления от бота
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
//Проверяем что от пользователья пришло именно текстовое сообщение
if reflect.TypeOf(update.Message.Text).Kind() == reflect.String && update.Message.Text != "" {
switch update.Message.Text {
case "/start":
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Hi, i'm a wikipedia bot, i can search information in a wikipedia, send me something what you want find in Wikipedia.")
bot.Send(msg)
case "/number_of_users":
if os.Getenv("DB_SWITCH") == "on" {
//Присваиваем количество пользоватьелей использовавших бота в num переменную
num, err := getNumberOfUsers()
if err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error.")
bot.Send(msg)
}
//Создаем строку которая содержит колличество пользователей использовавших бота
ans := fmt.Sprintf("%d peoples used me for search information in Wikipedia", num)
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, ans)
bot.Send(msg)
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database not connected, so i can't say you how many peoples used me.")
bot.Send(msg)
}
default:
//Устанавливаем язык для поиска в википедии
language := os.Getenv("LANGUAGE")
//Создаем url для поиска
ms, _ := urlEncoded(update.Message.Text)
url := ms
request := "https://" + language + ".wikipedia.org/w/api.php?action=opensearch&search=" + url + "&limit=3&origin=*&format=json"
//Присваем данные среза с ответом в переменную message
message := wikipediaAPI(request)
if os.Getenv("DB_SWITCH") == "on" {
//Отправляем username, chat_id, message, answer в БД
if err := collectData(update.Message.Chat.UserName, update.Message.Chat.ID, update.Message.Text, message); err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error, but bot still working.")
bot.Send(msg)
}
}
//Проходим через срез и отправляем каждый элемент пользователю
for _, val := range message {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, val)
bot.Send(msg)
}
}
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Use the words for search.")
bot.Send(msg)
}
}
}
func main() {
time.Sleep(1 * time.Minute)
//Создаем таблицу
if os.Getenv("CREATE_TABLE") == "yes" {
if os.Getenv("DB_SWITCH") == "on" {
if err := createTable(); err != nil {
panic(err)
}
}
}
time.Sleep(1 * time.Minute)
//Вызываем бота
telegramBot()
}
package main
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/Syfaro/telegram-bot-api"
_ "github.com/lib/pq"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"time"
)
type SearchResults struct {
ready bool
Query string
Results []Result
}
type Result struct {
Name, Description, URL string
}
func (sr *SearchResults) UnmarshalJSON(bs []byte) error {
array := []interface{}{}
if err := json.Unmarshal(bs, &array); err != nil {
return err
}
sr.Query = array[0].(string)
for i := range array[1].([]interface{}) {
sr.Results = append(sr.Results, Result{
array[1].([]interface{})[i].(string),
array[2].([]interface{})[i].(string),
array[3].([]interface{})[i].(string),
})
}
return nil
}
func wikipediaAPI(request string) (answer []string) {
//Создаем срез на 3 элемента
s := make([]string, 3)
//Отправляем запрос
if response, err := http.Get(request); err != nil {
s[0] = "Wikipedia is not respond"
} else {
defer response.Body.Close()
//Считываем ответ
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
//Отправляем данные в структуру
sr := &SearchResults{}
if err = json.Unmarshal([]byte(contents), sr); err != nil {
s[0] = "Something going wrong, try to change your question"
}
//Проверяем не пустая ли наша структура
if !sr.ready {
s[0] = "Something going wrong, try to change your question"
}
//Проходим через нашу структуру и отправляем данные в срез с ответом
for i := range sr.Results {
s[i] = sr.Results[i].URL
}
}
return s
}
//Конвертируем запрос для использование в качестве части URL
func urlEncoded(str string) (string, error) {
u, err := url.Parse(str)
if err != nil {
return "", err
}
return u.String(), nil
}
var host = os.Getenv("HOST")
var port = os.Getenv("PORT")
var user = os.Getenv("USER")
var password = os.Getenv("PASSWORD")
var dbname = os.Getenv("DBNAME")
var sslmode = os.Getenv("SSLMODE")
var dbInfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbname, sslmode)
//Собираем данные полученные ботом
func collectData(username string, chatid int64, message string, answer []string) error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Конвертируем срез с ответом в строку
answ := strings.Join(answer, ", ")
//Создаем SQL запрос
data := `INSERT INTO users(username, chat_id, message, answer) VALUES($1, $2, $3, $4);`
//Выполняем наш SQL запрос
if _, err = db.Exec(data, `@`+username, chatid, message, answ); err != nil {
return err
}
return nil
}
//Создаем таблицу users в БД при подключении к ней
func createTable() error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Создаем таблицу users
if _, err = db.Exec(`CREATE TABLE users(ID SERIAL PRIMARY KEY, TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, USERNAME TEXT, CHAT_ID INT, MESSAGE TEXT, ANSWER TEXT);`); err != nil {
return err
}
return nil
}
func getNumberOfUsers() (int64, error) {
var count int64
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return 0, err
}
defer db.Close()
//Отправляем запрос в БД для подсчета числа уникальных пользователей
row := db.QueryRow("SELECT COUNT(DISTINCT username) FROM users;")
err = row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func telegramBot() {
//Создаем бота
bot, err := tgbotapi.NewBotAPI(os.Getenv("TOKEN"))
if err != nil {
panic(err)
}
//Устанавливаем время обновления
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
//Получаем обновления от бота
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
//Проверяем что от пользователья пришло именно текстовое сообщение
if reflect.TypeOf(update.Message.Text).Kind() == reflect.String && update.Message.Text != "" {
switch update.Message.Text {
case "/start":
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Hi, i'm a wikipedia bot, i can search information in a wikipedia, send me something what you want find in Wikipedia.")
bot.Send(msg)
case "/number_of_users":
if os.Getenv("DB_SWITCH") == "on" {
//Присваиваем количество пользоватьелей использовавших бота в num переменную
num, err := getNumberOfUsers()
if err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error.")
bot.Send(msg)
}
//Создаем строку которая содержит колличество пользователей использовавших бота
ans := fmt.Sprintf("%d peoples used me for search information in Wikipedia", num)
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, ans)
bot.Send(msg)
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database not connected, so i can't say you how many peoples used me.")
bot.Send(msg)
}
default:
//Устанавливаем язык для поиска в википедии
language := os.Getenv("LANGUAGE")
//Создаем url для поиска
ms, _ := urlEncoded(update.Message.Text)
url := ms
request := "https://" + language + ".wikipedia.org/w/api.php?action=opensearch&search=" + url + "&limit=3&origin=*&format=json"
//Присваем данные среза с ответом в переменную message
message := wikipediaAPI(request)
if os.Getenv("DB_SWITCH") == "on" {
//Отправляем username, chat_id, message, answer в БД
if err := collectData(update.Message.Chat.UserName, update.Message.Chat.ID, update.Message.Text, message); err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error, but bot still working.")
bot.Send(msg)
}
}
//Проходим через срез и отправляем каждый элемент пользователю
for _, val := range message {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, val)
bot.Send(msg)
}
}
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Use the words for search.")
bot.Send(msg)
}
}
}
func main() {
time.Sleep(1 * time.Minute)
//Создаем таблицу
if os.Getenv("CREATE_TABLE") == "yes" {
if os.Getenv("DB_SWITCH") == "on" {
if err := createTable(); err != nil {
panic(err)
}
}
}
time.Sleep(1 * time.Minute)
//Вызываем бота
telegramBot()
}
Давайте разнесем код по отдельным файлам чтобы он был более читабельным.
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
)
type SearchResults struct {
ready bool
Query string
Results []Result
}
type Result struct {
Name, Description, URL string
}
func (sr *SearchResults) UnmarshalJSON(bs []byte) error {
array := []interface{}{}
if err := json.Unmarshal(bs, &array); err != nil {
return err
}
sr.Query = array[0].(string)
for i := range array[1].([]interface{}) {
sr.Results = append(sr.Results, Result{
array[1].([]interface{})[i].(string),
array[2].([]interface{})[i].(string),
array[3].([]interface{})[i].(string),
})
}
return nil
}
func wikipediaAPI(request string) (answer []string) {
//Создаем срез на 3 элемента
s := make([]string, 3)
//Отправляем запрос
if response, err := http.Get(request); err != nil {
s[0] = "Wikipedia is not respond"
} else {
defer response.Body.Close()
//Считываем ответ
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
//Отправляем данные в структуру
sr := &SearchResults{}
if err = json.Unmarshal([]byte(contents), sr); err != nil {
s[0] = "Something going wrong, try to change your question"
}
//Проверяем не пустая ли наша структура
if !sr.ready {
s[0] = "Something going wrong, try to change your question"
}
//Проходим через нашу структуру и отправляем данные в срез с ответом
for i := range sr.Results {
s[i] = sr.Results[i].URL
}
}
return s
}
package main
import (
"net/url"
)
//Конвертируем запрос для использование в качестве части URL
func urlEncoded(str string) (string, error) {
u, err := url.Parse(str)
if err != nil {
return "", err
}
return u.String(), nil
}
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"os"
"strings"
)
var host = os.Getenv("HOST")
var port = os.Getenv("PORT")
var user = os.Getenv("USER")
var password = os.Getenv("PASSWORD")
var dbname = os.Getenv("DBNAME")
var sslmode = os.Getenv("SSLMODE")
var dbInfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbname, sslmode)
//Собираем данные полученные ботом
func collectData(username string, chatid int64, message string, answer []string) error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Конвертируем срез с ответом в строку
answ := strings.Join(answer, ", ")
//Создаем SQL запрос
data := `INSERT INTO users(username, chat_id, message, answer) VALUES($1, $2, $3, $4);`
//Выполняем наш SQL запрос
if _, err = db.Exec(data, `@`+username, chatid, message, answ); err != nil {
return err
}
return nil
}
//Создаем таблицу users в БД при подключении к ней
func createTable() error {
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return err
}
defer db.Close()
//Создаем таблицу users
if _, err = db.Exec(`CREATE TABLE users(ID SERIAL PRIMARY KEY, TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, USERNAME TEXT, CHAT_ID INT, MESSAGE TEXT, ANSWER TEXT);`); err != nil {
return err
}
return nil
}
func getNumberOfUsers() (int64, error) {
var count int64
//Подключаемся к БД
db, err := sql.Open("postgres", dbInfo)
if err != nil {
return 0, err
}
defer db.Close()
//Отправляем запрос в БД для подсчета числа уникальных пользователей
row := db.QueryRow("SELECT COUNT(DISTINCT username) FROM users;")
err = row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
package main
import (
"fmt"
"github.com/Syfaro/telegram-bot-api"
"os"
"reflect"
"time"
)
func telegramBot() {
//Создаем бота
bot, err := tgbotapi.NewBotAPI(os.Getenv("TOKEN"))
if err != nil {
panic(err)
}
//Устанавливаем время обновления
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
//Получаем обновления от бота
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
//Проверяем что от пользователья пришло именно текстовое сообщение
if reflect.TypeOf(update.Message.Text).Kind() == reflect.String && update.Message.Text != "" {
switch update.Message.Text {
case "/start":
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Hi, i'm a wikipedia bot, i can search information in a wikipedia, send me something what you want find in Wikipedia.")
bot.Send(msg)
case "/number_of_users":
if os.Getenv("DB_SWITCH") == "on" {
//Присваиваем количество пользоватьелей использовавших бота в num переменную
num, err := getNumberOfUsers()
if err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error.")
bot.Send(msg)
}
//Создаем строку которая содержит колличество пользователей использовавших бота
ans := fmt.Sprintf("%d peoples used me for search information in Wikipedia", num)
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, ans)
bot.Send(msg)
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database not connected, so i can't say you how many peoples used me.")
bot.Send(msg)
}
default:
//Устанавливаем язык для поиска в википедии
language := os.Getenv("LANGUAGE")
//Создаем url для поиска
ms, _ := urlEncoded(update.Message.Text)
url := ms
request := "https://" + language + ".wikipedia.org/w/api.php?action=opensearch&search=" + url + "&limit=3&origin=*&format=json"
//Присваем данные среза с ответом в переменную message
message := wikipediaAPI(request)
if os.Getenv("DB_SWITCH") == "on" {
//Отправляем username, chat_id, message, answer в БД
if err := collectData(update.Message.Chat.UserName, update.Message.Chat.ID, update.Message.Text, message); err != nil {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Database error, but bot still working.")
bot.Send(msg)
}
}
//Проходим через срез и отправляем каждый элемент пользователю
for _, val := range message {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, val)
bot.Send(msg)
}
}
} else {
//Отправлем сообщение
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Use the words for search.")
bot.Send(msg)
}
}
}
func main() {
time.Sleep(1 * time.Minute)
//Создаем таблицу
if os.Getenv("CREATE_TABLE") == "yes" {
if os.Getenv("DB_SWITCH") == "on" {
if err := createTable(); err != nil {
panic(err)
}
}
}
time.Sleep(1 * time.Minute)
//Вызываем бота
telegramBot()
}
Теперь когда у нас есть наш бот напишем Dockerfile для него.
FROM alpine
ENV LANGUAGE="en"
COPY /code/code .
RUN apk add --no-cache ca-certificates && chmod +x code
EXPOSE 80/tcp
CMD [ "./code" ]
А так же docker-compose.yml для того чтобы нам не приходилось руками поднимать БД в случае если БД будет у нас на той же машине что и бот.
version: '3.5'
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD: test
bot:
image: trigun117/wikipedia-telegram-bot
environment:
CREATE_TABLE: "yes"
DB_SWITCH: "on"
TOKEN:
HOST: db
PORT: 5432
USER: postgres
PASSWORD: test
DBNAME: postgres
SSLMODE: disable
Разворачиваем бота
Развернем бота на локальном ПК, Amazon Web Service: EC2 и БД RDS, Google Cloud Platform: GCE и БД SQL.
Docker-Compose
Вставляем в наш docker-compose.yml токен для бота.
И запускаем docker-compose.
Ждем примерно 3 минуты и наш бот готов к работе.
Google Cloud Platform
Переходим в раздел SQL в GCP и создаем БД.
В разрешенных сетях укажем что трафик может идти с любого источника.
Теперь у нас есть БД и IP для подключения к ней.
Далее переходим на вкладку Compute Engine и создаем инстанс на котором будет развернут бот.
Так как у Google Compute Engine есть такая штука как Автоматизация, которая позволяет нам выполнить какие либо команды на инстансе после его создания без нашего участия то воспользуемся ею для комфортной развертки.
На вкладке Автоматизация укажем команды для установки docker и запуска контейнера с нашим ботом.
curl -s https://raw.githubusercontent.com/trigun117/TelegramBot-Go/master/installdocker.sh | bash && sudo docker run -e CREATE_TABLE=yes -e DB_SWITCH=on -e TOKEN=497014514:AAGKayv3tUxNrFWCmqtEIxKAS1TMvhXGdLE -e HOST=35.189.116.57 -e PORT=5432 -e USER=postgres -e PASSWORD=postgres -e DBNAME=postgres -e SSLMODE=disable -d trigun117/wikipedia-telegram-bot
Ждем около 5 минут и наш бот готов к использованию.
Amazon Web Service
Создаем БД.
Ждем пока создастся БД и переходим к ней для получения endpoint и дальнейшего подключения к БД с помощью него.
Далее переходим на вкладку EC2, Security Groups и выбираем группу в которой находится наша БД.
Внизу на вкладке Inbound жмем Edit и изменяем наше правило так чтобы входящие соединения шли с любого источника
и жмем Save.
На вкладке Instances, создаем новый инстанс и проходим все шаги создания. После создания подключаемся удобным для вас способом и выполняем комманду которую мы уже видели ранее.
Ждем около 3 минут и наш бот готов к работе.
curl -s https://raw.githubusercontent.com/trigun117/TelegramBot-Go/master/installdocker.sh | bash && sudo docker run -e CREATE_TABLE=yes -e DB_SWITCH=on -e TOKEN=497014514:AAGKayv3tUxNrFWCmqtEIxKAS1TMvhXGdLE -e HOST=test-habr.c2ugekubflbp.eu-west-2.rds.amazonaws.com -e PORT=5432 -e USER=postgres -e PASSWORD=postgres -e DBNAME=postgres -e SSLMODE=disable -d trigun117/wikipedia-telegram-bot
Заключение
Подводя итог, хочется сказать, что написание бота на Go было довольно-таки интересным и познавательным опытом. А также дало возможность подтянуть старые и получить новые знания.
Репозиторий на GitHub
Благодарю за уделенное внимание и приглашаю всех поделится своим мнением и мыслями в комментариях.
Комментарии (19)
grozaman
13.03.2018 12:20Тоже пишу своих ботов на Golang. Очень приятные получаются, особенно после JavaScript (Node.js), правда предпочитаю NoSQL базы.
Не знаю интересно ли кому-то тут почитать про какие-то связки с MTProto?AlexComin
13.03.2018 16:24По мне на JS(Node.js) делается куда проще и по приятнее. Но это дело вкуса.
grozaman
13.03.2018 16:37Писать может быстрее и может проще. Но на Go приложения получаются с ощущением «железобетонной» надежности, а главное намного быстрей.
gudvinr
13.03.2018 19:45Когда вы говорите "быстрей", тем более намного, то правила хорошего тона рекомендуют показывать сравнения, которые позволяют вам такие заявления делать.
В зависимости от задачи это может быть и не так.
grozaman
13.03.2018 20:03Хорошо :)
В случае с ботами я работал с генерацией графики, там прирост был очень большой. Буквально в два-три раза. В случае с Node использовал Fabric.js, в случае с Go использовал библиотеку gg.
Остальное мне показалось субъективно быстрей, но какие-то замеры не делал.
KirEv
13.03.2018 15:08а почему sql.Open не вынести из функций?
trigun117 Автор
13.03.2018 18:20Потому что вне функций можно использовать только (package, import, var, const, type, func), а мне нужно вдруг чего ловить err.
db, err := sql.Open("postgres", dbInfo) if err != nil { return err } defer db.Close()
Как вариант можно создать функцию openDB и в нее засунуть код который подключается к БД.
Laney1
13.03.2018 15:22в функции
wikipediaAPI
— зачем читать тело запроса в строку, когда можно сразу сделать Decode в нужную структуру? Я конечно понимаю, что есть статьи "никогда не пользуйтесь json.Decode, это небезопасно", но ИМХО это дуть на воду.
Еще не нравится куча кастов к типу
interface{}
в функцииUnmarshalJSON
— это очень медленно
HoberMallow
13.03.2018 16:24Тоже сейчас пишу бота, но обновления решил получать через вебхук, а не лонгполлинг — есть ощущение, что так ресурсов сервера тратится меньше. Не расскажите, почему выбрали опрос, а не вебхук, подводные камни, связанные с той или иной стратегией?
trigun117 Автор
13.03.2018 17:36Вы правы, вебхук тратит намного меньше ресурсов, так же он намного надежнее, ибо при обновлениях вам их присылают сервера телеграма т.е. вашему приложению не нужно открывать соединение с серверами каждые n секунды чтобы получить новые данные. Проще говоря, используя long polling вашему приложению самому нужно запрашивать обновления у API, а используя вебхуки — сервера телеграма сами будут отправлять на ваш сервер каждое обновление. Мною был выбран long polling ибо он проще, однако для боевого бота который шел бы в продакшен однозначно вебхук.
scab
14.03.2018 08:49Мне одному кажется что токен бота надо бы замазывать в скринах?
Mr_Tabrest_3115
14.03.2018 10:32Бот ведь создан для примера, никакого вреда автору не принести использованием токена. Тем более он легко меняется, буквально одной командой, направленной к botfather
scab
15.03.2018 15:47О таких вещах легко забыть, учитывая что это исключительно ЧФ. Да и вообще приучить себя не выкладывать такие вещи в публичный доступ, как по мне, хорошая привычка, а то зайдешь на clip2net в публичную галерею и ужасаешься =)
astec
14.03.2018 11:58Мой фрэймфорк на Go для написания ботов под разные мессенджеры с единой логикой — https://github.com/strongo/bots-framework — может кому пригодится.
В основном заточен на Телеграм и работает на Google AppEngine.
Bidofan
Thanks, very interesting