Предыстория
На ЛитРес есть система бонусов и купонов, которые появляются с завидной регулярностью. Чтобы сделать приятное жене, да и в целом может найти себе интересную книжку, начал мониторить сайт в котором появляются свежие купоны и скидывал их в телеграм. Но буквально спустя несколько дней мне это дело надоело и я решил автоматизировать данный процесс так, чтобы он мог быть доступен всем кто этого хочет.
Реализация
Так как я постоянно постил новые купоны в телеграм, да и в целом мне нравится этот инструмент, решил создать еще одного бота для телеграма, благо дело для него уже создано достаточное количество библиотек. Возьмем в качестве языка golang и библиотеку telegram-bot-api. Так же нам нужно выбрать ресурс с которого можно было бы тянуть информацию, у меня на примете было несколько сайтов и я думал в целом написать универсальный парсер, но в какой-то момент мне стало лень, и я решил остановить свой выбор на одном ресурсе. Для того чтобы хранить купоны даже после рестарта, решил воспользоваться простой базой sqlite3. В ней будем хранить информацию о купонах, а так же информацию о зарегистрированных пользователях в телеграм боте, так же информацию о том какие купоны пользователь уже получил и какие ещё нет.
Выглядит это примерно так.
Парсинг сайта
Парсингом сайта у нас займется библиотека goquery — работает примерно так же как и jquery.
Оперируя структурой goquery.Document вытягиваем html теги нужные нам. Получаем дату, код, ссылку и описание купона. Купоны бывают разные поэтому нам нужно взять описание чтобы точно знать что он дает. Время на сайте отображается в нескольких форматах значит его мы будем преобразовывать unixtime, для того чтобы потом можно было удобно проверять скор действия купонов. Ссылку тоже изменим, для того чтобы она отработала и перевела нас на сайт ЛитРес, иначе возникнет ошибка. Связана она с тем, что ссылка ведет на сайт из которого мы вытягиваем купоны, и если мы перешли туда воспользовавшись прямой ссылкой не через сайт то js на этой странице не отрабатывает и мы не получаем желаемого результата.
Telegram bot
Как настраивать бота с помощью BotFather я рассказывать не буду, на сайте telegram есть отличная инструкция. В telegram api есть несколько способов получать события произошедшие с ботом, первый это простой http запрос который отдаст все в виде струтуры и второй это websocket. Изначально я не собирался использовать свой сервак по этому воспользовался первым способом, благо дело что в telegram-bot-api есть updater и разницу вы не почувствуете.
type SNBot struct { cfg *Config bot *tgbotapi.BotAPI upd tgbotapi.UpdatesChannel } func New(cfg *Config) (*SNBot, error) { bot, err := tgbotapi.NewBotAPI(cfg.Token) if err != nil { return nil, err } level.Info(cfg.Logger).Log("msg", "Authorized on account", "bot-name", bot.Self.UserName) u := tgbotapi.NewUpdate(0) u.Timeout = cfg.UpdateTime updates, err := bot.GetUpdatesChan(u) if err != nil { return nil, err } return &SNBot{ cfg: cfg, bot: bot, upd: updates, }, nil }
После создания базы, бота и парсера мы должны задать время через которое будет работать наш бот и делать рассылку, для этого воспользуемся gocron. Сначала парсер опрашивает сайт потом task которую мы создали в gocron собирает все чаты из storage и отправляет ещё не отправленные купоны из storage в чат юзера.
func task(bot *snbot.SNBot, s *storage.Storage, c *collector.Collector, logger kitlog.Logger) { c.Collect(collector.ConditionQuery{ URI: "https://lovikod.ru/knigi/promokody-litres", }) chats, err := s.GetChat() if err != nil { level.Error(logger).Log("msg", "failed get chats", "err", err) } for _, id := range chats { records, err := s.GetNotUseCoupon(id) if err != nil { level.Error(logger).Log("msg", "failed get coupons", "err", err) return } var msg string for i, rec := range records { msg = fmt.Sprintf("%v%v:\t%s \nКод--->: %s\nВремя истечения: %v\nОписание: %s\n\n", msg, i+1, rec.Link, rec.Code, time.Unix(rec.Date, 0).Format("02.01.2006"), rec.Description) } if len(msg) != 0 { err = bot.Send(id, msg) if err != nil { level.Error(logger).Log("msg", "failed send message", "err", err) continue } err = s.MarkAsRead(id, records) if err != nil { level.Error(logger).Log("msg", "failed marked as read", "err", err) continue } } } level.Info(logger).Log("msg", "send all chats new coupons") }
Из-за того что количество купонов бывает намного больше, чем разрешено отправлять в чат, пришлось сделать искусственное ограничение на количество купонов за один раз до пяти и добавить спец кнопку, которая позволяет получить все ещё не полученные купоны.
func (s *SNBot) Send(chatID int64, msg string) error { level.Error(s.cfg.Logger).Log("msg", "try send", "chatID", chatID) var numericKeyboard = tgbotapi.NewReplyKeyboard( tgbotapi.NewKeyboardButtonRow( tgbotapi.NewKeyboardButton("/print5"), ), ) m := tgbotapi.NewMessage(chatID, msg) m.ReplyMarkup = numericKeyboard _, err := s.bot.Send(m) if err != nil { if err.Error() == errBlockedByUser { s.cfg.Storage.UpdChatActivity(chatID, false) } return err } return nil }
Создание Dockerfile
Какое нынче приложение да и без докера, создадим докер образ.
# build binary FROM golang:1.10.3-alpine3.8 AS build RUN apk add --no-cache linux-headers gcc g++ ARG VERSION=dev WORKDIR /go/src/github.com/wenkaler/xfreehack COPY . /go/src/github.com/wenkaler/xfreehack RUN CGO_ENABLED=1 go build \ -o /out/xfree \ -ldflags "-X main.serviceVersion=$VERSION" \ github.com/wenkaler/xfreehack/cmd # copy to alpine image FROM alpine:3.8 WORKDIR /app RUN mkdir /db COPY --from=build /out/xfree /app RUN apk add --no-cache tzdata RUN apk --no-cache add ca-certificates ENV TZ Europe/Moscow RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone CMD ["/app/xfree"]
Systemd
В итоге я все же купил виртуалку. Но как оказалось на ней стоит ядро версии 2.6, поэтому я не стал париться с его upgrad-ом и просто все запустил через systemd. Зачем? — просто докер не может быть установлен на систему с ядром ниже версии 3.
[Unit] Description=Xfree service After=network.target After=network-online.target [Service] ExecStart=/urs/local/bin/xfree Environment="TELEGRAM_TOKEN=$TELEGRAM_TOKEN" "PATH_DB=/db/xfree.db" TimeoutSec=30 Restart=on-failure RestartSec=30 [Install] WantedBy=multi-user.target
Вывод
Теперь жена довольна, что может получать и бесплатные книги на ЛитРес, ну а мне было просто интересно решить эту проблему.Есть ещё что можно улучшить, добавить систему оповещения если не сработает ф-я MarkAsRead (пока что такого не было, но мало ли), так же он сейчас отменяет подписку и больше не рассылает сообщения людям которые отписались от него и нужно вернуть их в активное состояние после повторного нажатия команды /start. Ну и в целом добавить возможность выбора времени рассылки и выбора купонов, ведь на сайте есть не только купоны от ЛитРес. Но это все по необходимости, пока что таких заявок не поступало.
Ссылки
- Сам проект
- Сайт с купонами
- Имя бота @xFreeCouponBot
ссылка на оригинал статьи https://habr.com/ru/post/494460/
Добавить комментарий