Автоматизация сбора купонов для бесплатной литературы

от автора

Предыстория

На ЛитРес есть система бонусов и купонов, которые появляются с завидной регулярностью. Чтобы сделать приятное жене, да и в целом может найти себе интересную книжку, начал мониторить сайт в котором появляются свежие купоны и скидывал их в телеграм. Но буквально спустя несколько дней мне это дело надоело и я решил автоматизировать данный процесс так, чтобы он мог быть доступен всем кто этого хочет.

Реализация

Так как я постоянно постил новые купоны в телеграм, да и в целом мне нравится этот инструмент, решил создать еще одного бота для телеграма, благо дело для него уже создано достаточное количество библиотек. Возьмем в качестве языка 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 в чат юзера.

Task function

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. Ну и в целом добавить возможность выбора времени рассылки и выбора купонов, ведь на сайте есть не только купоны от ЛитРес. Но это все по необходимости, пока что таких заявок не поступало.

Ссылки

  1. Сам проект
  2. Сайт с купонами
  3. Имя бота @xFreeCouponBot

ссылка на оригинал статьи https://habr.com/ru/post/494460/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *