«Убийца» RSS-ридеров: переносим ленту RSS в Telegram

Опубликовано 0 комментариев 47 просмотров

Я фрилансер, в погоде за заказами стремящийся первым получать информацию о новых заказах на бирже которой пользуюсь активнее всего. А еще я активно использую в работе и за ее пределами активно блокируемый на территории необъятной мессенджер.

Поэтому когда Дуров в далеком 2015 объявил о запуске ботов для Telegram с публичным API я ощутимо обрадовался. Первым делом перетащив все уведомления с форм обратной связи с морально устаревшего для этих целей email`а я задумался и о других вещах.

В момент рождения идеи я был активным исполнителем на бирже для фрилансеров Weblancer. Она была хороша всем (и остается такой по сей день, на мой взгляд), но в контексте статьи возьмем только одно преимущество - персональная лента, сформированная на основе фильтра категорий с сайта была доступна в формате RSS. А что может быть лучше, чем получение уведомлений о новых интересных для меня как фрилансера заказах через единый интерфейс, доступный как на смартфоне так и на десктопе сразу после их публикации?

АХТУНГ! Данный мануал содержит вполне себе актуальный код, но написан для Laravel и его планировщика задач (хотя по факту, заменив с десяток строк и запустив все просто из-под крона вы получите тот же самый результат).

Подготовка

Создадим бота отправив @BotFather команду /newbot. Получим токен API.

Переносим ленту RSS в Telegram

Отлично. Напишем любое сообщение боту, откроем страницу https://api.telegram.org/bot<TOKEN>/getUpdates. В ответном json`е найдем result->message->chat->id и запомним его.

Хранение уведомлений

Переносим ленту RSS в Telegram

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

  • сохранять время публикации pubDate последней записи
  • сохранять вообще все записи

Места на диске мне не жалко (к слову, 9000 строк что сейчас сохранены в базе занимают всего ~2.5мб) а информация когда-нибудь может пригодиться, поэтому я выбрал второй вариант.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateWeblancerTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('weblancer', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('link');
            $table->timestamp('created_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('weblancer');
    }
}

Нам не надо многого: всего лишь название name, ссылку на запись link и время публикации created_at, поэтому таблица получается весьма компактной.

Пишем задание

Оформим скелет класса.

<?php

namespace App\Console\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;

class TelegramWeblancer extends Command
{
    protected $signature = 'telegram:weblancer';

    protected $description = '';

    protected $url;

    protected $userId;

    protected $botToken;

    protected $table;

    public function __construct()
    {
        parent::__construct();
        $this->table = \DB::table('weblancer');
        $this->userId = config('gtxtymt.telegram.user_id');
        $this->botToken = config('gtxtymt.telegram.bot_token');
    }

    public function handle()
    {
        //
    }
}
  • $url - адрес ленты RSS
  • $userId - ID юзера, полученный ранее
  • $botToken - токен бота, полученный ранее

Метод handle

Получим и конвертируем в массив RSS-ленту.

$data = simplexml_load_file($this->url);
$data = json_decode(json_encode($data), true); // чуть-чуть костыльно. альтернатива - https://gist.github.com/jasondmoss/7344311, например

Перепроверим полученные данные на предмет того, является ли лента массивом или возникла ошибка. Создадим удобную коллекцию.

if(!isset($data['channel']['item']) || !is_array($data['channel']['item'])) {
    $this->error('Undefined data.');
    return;
}

$data = collect($data['channel']['item']);

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

if($this->table->count() == 0) {
    $this->insert($data->first());
    $this->info('Success.');
    return;
}

$lastUpdate = Carbon::parse($this->table->orderByDesc('created_at')->first()->created_at);

$data
    ->filter(function($value) use ($lastUpdate) {
        return Carbon::parse($value['pubDate'])->setTimezone(config('app.timezone')) > $lastUpdate;
    })
    ->reverse()
    ->each([$this, 'insert']);

$this->info('Success.');

Метод insert

Так как в handle обработка записей происходит в двух местах, имеет смысл сам процесс вывести в отдельный метод дабы не было дублей.

public function insert(array $i)
{
    $this->table->insert([
        'name' => $i['title'],
        'link' => $i['link'],
        'created_at' => Carbon::parse($i['pubDate'])->setTimezone(config('app.timezone'))
    ]);

    $message = '<strong>'.$i['title'].'</strong>'.PHP_EOL.PHP_EOL.'<a href="'.$i['link'].'">Открыть</a>';
    $this->notify($i);
}

Метод notify

Метод содержит в себе лишь инициализацию cURL и отправку сообщения на сервера Telegram.

private function notify(string $message)
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, 'https://api.telegram.org/bot'.$this->botToken.'/sendMessage');
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, [
        'chat_id' => $this->userId,
        'message' => $message,
        'disable_web_page_preview' => true,
        'parse_mode' => 'html'
    ]);
    curl_exec($curl);
    curl_close($curl);
}

Добавляем команду в список задач

Цель данного кода - получить уведомления сразу, как только они стали доступны в ленте. Поэтому имеет смысл поставить минимальный интервал выполнения - 1 минута.

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        $schedule
            ->command('telegram:weblancer')
            ->everyMinute();
    }
}

Заключение

Данный мануал позволит легко избавиться от нескольких приложений, если вы все еще читаете ленты RSS. Он не раскрывает все прелести работы с API Telegram, но дает начальную информацию о нем.

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

Удачи.

В ответ на сообщение

Доступна разметка Markdown. А еще вы можете использовать крутой пак эмоций.

Нажимая на кнопку «Отправить» вы даете свое согласие на обработку персональных данных в соответствии с законом №152-ФЗ «О персональных данных» от 27.07.2006 и принимаете условия Политики конфеденциальности.