X-Notifier. Пишем оповещалку для трекера и диалогов на Хабарахабр

  • Tutorial

Есть хороший плагин для всех популярных браузеров, X-Notifier. Он позволяет получать уведомления о новых письмах, сообщениях, да о чем угодно с различных сервисов в одном месте. Для X-Notifier написано множество скриптов (Gmail, Яндекс.Почта, Google+, Facebook, Twitter и прочих). Но скрипта для Хабра, до сих пор никто не написал, пора исправить это недоразумение!

Вступление

Скрипт созданный по методам используемыми в этом посте, с большой вероятностью будет работать в любом браузере, для которого существует это дополнение. Скрипт написанный для Хабра, тестировался только в Firefox и Google Chrome, в последнем он работает с ограничениями. Так же на данном этапе поддерживается одновременная работа только с одним аккаунтом. Для тех, кто не хочет читать статью, а просто хочет оповещалку, ссылка на заключение.

Изучение цели

Если описать процесс поверхностно, то нам нужно сделать следующее. Отправить запрос с данными на авторизацию, получить куки и время от времени получать страницу авторизованного пользователя и парсить счетчики трекера и диалогов. Все предельно просто!
Рассмотрим форму авторизации на странице https://id.tmtm.ru/login/ (лишние детали убраны):
Форма


<form novalidate="" data-remote="true" method="post" id="login_form" class="s-form login_form validateble"
      action="/ajax/login/">

    <input type="hidden" value="180351c318af67fa0ec59ecad9ebae72" name="state">
    <input type="hidden" value="habrahabr" name="consumer">

    <div class="s-field s-with-error">
<input type="email" data-validate_url="/ajax/validate/email/" id="email_field" tabindex="1" autofocus="" data-required="true" name="email" placeholder="E-mail" value="">
    </div>

    <div class="s-field s-with-error">
        <input type="password" tabindex="2" name="password" data-required="true" placeholder="Пароль" value="">
    </div>

    <div class="s-field">
        <input type="hidden" name="captcha">
        <input type="hidden" id="recaptcha_challenge_field" name="recaptcha_challenge_field">
        <input type="text" name="recaptcha_response_field" id="recaptcha_response_field" data-required="true"
               placeholder="Символы с картинки" value="" autocomplete="off" tabindex="3">
        <div class="icon_captcha"></div>

        <script src="//www.google.com/recaptcha/api/challenge?k=6LftHuoSAAAAAORONRXn_6xb2f_QCtXqfbRPfY2e"
                type="text/javascript">
        </script>
        
        <input type="hidden" value="recaptcha" name="captcha_type">
    </div>
</form>


Здесь мы видим 3 видимых поля для отправки (email, password, recaptcha_response_field), с ними все понятно. А так же 5 скрытых (state, consumer, captcha, recaptcha_challenge_field, captcha_type). Поле state представляет собой некий уникальный идентификатор который генерируется для каждого логина, consumer для нас это статичное значение и всегда равно habrahabr, значение captcha отправляется всегда пустое, recaptcha_challenge_field уникальный идентификатор капчи, captcha_type всегда равно recaptcha.
С формой все понятно, теперь перейдем к данным для счетчика. Это панель пользователя.
Панель

<div class="userpanel silver">
    <div class="bottom">
        <a href="http://habrahabr.ru/tracker/">трекер</a>
        <a class="count" href="http://habrahabr.ru/tracker/">+2</a>
         <a href="http://habrahabr.ru/conversations/">диалоги</a>            
        <a href="http://habrahabr.ru/users/BloodUnit/favorites/">избранное</a>
    </div>
  </div>


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

API

У плагина есть что-то вроде API, но документации к нему мне найти не удалось, и скорее всего, ее нет. Единственное что нашлось, это заметка как включить логи и небольшой пост от разработчика аддона, но он подходит только для сайтов с простой авторизацией, хабр не из таких. Поэтому пришлось читать исходники и пробовать, и пробовать…

Рассмотрим методы которые предстоит использовать.
Всего их нам понадобится 5:
  • init() — метод инициализации скрипта, запускается один раз при старте
  • getCount(aData) — метод принимающий строку(обычно это HTML), и возвращающий счетчик. Если счетчик >= 0, проверка проведена успешно, иначе проверка завершилась ошибкой
  • checkLogin(aData, aHttp) — проверяет состояние логина, aData строка(как правило HTML), aHttp объект XMLHttpRequest
  • process(aData, aHttp) — метод запускается на каждом этапе(ниже приведен список) работы плагина, aData строка данных(как правило HTML), aHttp объект XMLHttpRequest
  • dlog(name, data) — метод для записи в лог, принимает два строковых параметра

Список этапов, число это последовательность выполнения:
  • ST_CHECK = 0 — проверка авторизации
  • ST_PRE = 100 — подготовка к логину
  • ST_PRE_RES = 101 — прием ответа от запроса запущенного на предыдущем этапе
  • ST_LOGIN = 200 — логин
  • ST_LOGIN_RES = 201 — прием ответа от запроса запущенного на предыдущем этапе
  • ST_DATA = 300 — запрос данных для обработки, например подсчета непрочитанных сообщений
  • ST_DATA_RES = 301 — прием ответа от запроса запущенного на предыдущем этапе

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

Пишем

Для начала нам надо инициализировать скрипт. Здесь мы зададим некоторые статичные параметры и этап с которого предстоит выполнение скрипта.
Скрытый текст
function init() {
    this.initStage = ST_PRE; // По умолчанию, первым идет этап ST_LOGIN, но так как нам предстоит разгадывать капчу, то мы ставим подготовку
    this.loginData = ["https://id.tmtm.ru/ajax/login/", "email", "password", "consumer=habrahabr&captcha_type=recaptcha&captcha="]; // Первый элемент массива URL для постинга формы, второй и трейтий значения атрибутов name для полей e-mail и пароля, четвертый дополнительные параметры

    this.dataURL = "http://habrahabr.ru/"; // URL для парсинга
    this.viewURL = "http://habrahabr.ru/tracker/"; // URL который будет открываться при клике
    this.cookieDomain = "habrahabr.ru"; // Домен для которого будут ставиться куки
}


Следующий шаг — это авторизация, все этапы расположены в хронологическом порядке:
Скрытый текст
function process(aData, aHttp) {
    switch (this.stage) {
        /* Переходим по ссылке логина, чтобы хабр сгнерировал нам state */
        case ST_PRE:
            this.getHtml("https://auth.habrahabr.ru/login/");
            return false;
        /* Получаем страницу с формой, парсим state и ссылку на скрипт recaptcha,
            скачиваем скрипт и переходим к следующему этапу */
        case ST_PRE_RES:
            var recaptchaScriptLink = aData.match(/(\/\/www.google.com\/recaptcha\/api\/challenge\S+?)"/);
            var state = aData.match(/state=([\w\n]+)/);
            if (recaptchaScriptLink && state) {
                this.originPostData = this.loginData[LOGIN_POST];
                this.loginData[LOGIN_POST] += "&state=" + encodeURIComponent(state[1]);
                this.referer = this.loginData[LOGIN_URL] + "?" + "&state=" + encodeURIComponent(state[1]) + "&consumer=habrahabr";
                this.getHtml("https:" + recaptchaScriptLink[1]);
                return false;
            }
            this.onError();
            break;
        /* Получаем ссылку на капчу и выводим окно ввода пользователю */
        case ST_PRE_RES + 1:
            var recaptchaUid = aData.match(/challenge\s*:\s*'(\S+?)'/);
            if (recaptchaUid) {
                this.loginData[LOGIN_POST] += "&recaptcha_challenge_field=" + encodeURIComponent(recaptchaUid[1]);
                this.openCaptchaDialog(this.id, this.user, "https://www.google.com/recaptcha/api/image?c=" + recaptchaUid[1]);
                return false;
            }
            this.onError();
            break;
        /* Добавляем введенные пользователем данные в запрос */
        case ST_PRE_RES + 2:
            this.loginData[LOGIN_POST] += "&recaptcha_response_field=" + encodeURIComponent(aData);
            this.stage = ST_LOGIN;
            return this.process(aData, aHttp);
            break;
        /* Посылаем запрос авторизации */
        case ST_LOGIN:
            this.getHtml(this.loginData[LOGIN_URL], this.loginData[LOGIN_POST], {
                Referer: this.referer
            });
            return false;
        /* Обрабатываем запрос авторизации 
            и устанавлиаем следующим шагом получение данных для обработки */
        case ST_LOGIN_RES:
            this.loginData[LOGIN_POST] = this.originPostData;
            var habrRedirectLink = aData.match(/'(.*?)'/);
            if (habrRedirectLink) {
                this.getHtml(habrRedirectLink[1]);
            }
            this.stage = ST_DATA;
            return true;
    }
    return this.baseProcess(aData, aHttp);
}


Тут есть несколько моментов.
  • Javascript в контексте плагина не выполняется, поэтому нам необходимо делать лишние телодвижения для получения ссылки на капчу
  • Если этап возвращает false, будет инкрементировано значение последовательности.
  • Данные которые возвращают методы this.getHtml и this.openCaptchaDialog, будут переданы следующему этапу
  • На этапе ST_LOGIN, нам необходимо устанавливать значение Referer, иначе авторизация не пройдет. Google Chrome не позволяет устанавливать этот параметр (и это стандарт, хотя и в Working Draft), соответственно авторизация через плагин в нем работать не будет, но если вы залогинены на сайте, то все будет работать нормально!
  • На этапе ST_LOGIN, Хабр возвращает скрипт с редиректом на главную который выглядит примерно так:
    Скрытый текст
    window.location.href = 'https://habrahabr.ru/ac/entrance/?token=5a15a5d48c7fdeaed5ab20e852107dc6&state=26593fdea0963d8241aab3f20a6893b4&time=1390388377&sign=bb8f45d63c768ed6aebc5ae2bb22de3b';
    



Реализация метода проверки логина очень проста:
Скрытый текст
function checkLogin(aData, aHttp) {
    switch (this.stage) {
        /* Получаем HTML главной страницы */
        case ST_CHECK:
            this.getHtml(this.dataURL);
            return false;
        /* Ищем ссылку логина на ней, если не находим, залогинены */
        case ST_CHECK + 1:
            var loginLink = aData.match(/<a.+?class="login"/);
            if (!loginLink) {
                this.stage = ST_DATA;
                this.getHtml(this.dataURL);
                return true;
            } else {
                this.stage = this.initStage;
                return this.process("");
            }
    }
    this.onError();
    return true;
}


И наконец последний метод, парсинг счетчиков:
Скрытый текст
function getCount(aData) {
    var userMenu = aData.match(/userpanel[\s\S]*?charge_string/);
    if (!userMenu) {
        return -1;
    } else {
        var counter = 0;
        var counterRegex = /class="count"[^>]*>\+?(\d*)/g
        var counterResult;
        while ((counterResult = counterRegex.exec(userMenu[0])) !== null) {
            counter += +counterResult[1] || 0;
        }
        return counter;
    }
}


Тут пояснять даже особо нечего, просто выбираем значения регулярными выражениями, и возвращаем полученный результат.

Заключение

В данном посте был показан пример, как написать простой скрипт для проверки сайта на предмет новых сообщений. Следуя этим правилам, вы можете написать скрипт практически для любого сайта. Некоторые скрипты будут гораздо проще в написании, особенно если у сайта простая схема авторизации (например скрипт для Яндекс.Почты умещается в 30 строк).


Готовый скрипт для Хабра можно скачать со страницы скриптов для X-Notifier или взять на GitHub. Форки и пулл реквесты приветствуются.
Страница дополнения.
Share post

Comments 17

    0
    У меня вопрос. Сразу оговорюсь, я не эксперт в веб программировангии. Как Вам удалось аутентификацию Капчи пройти?
      0
      Если пользователь не авторизован, то ему выведется капча.
      Я написал комментарий в примере:
      Скрытый текст
      /* Получаем ссылку на капчу и выводим окно ввода пользователю */
              case ST_PRE_RES + 1:
                  var recaptchaUid = aData.match(/challenge\s*:\s*'(\S+?)'/);
                  if (recaptchaUid) {
                      this.loginData[LOGIN_POST] += "&recaptcha_challenge_field=" + encodeURIComponent(recaptchaUid[1]);
                      this.openCaptchaDialog(this.id, this.user, "https://www.google.com/recaptcha/api/image?c=" + recaptchaUid[1]);
                      return false;
                  }
                  this.onError();
                  break;
      


        0
        Спасибо за оперативный ответ.
      –2
      imageРасширение для Хабры давно написано мной больше года назад.

      image

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

      Сейчас добиваю (тестирую) новую версию с новыми возможностями + версия для Firefox уже готова, тестирую.

      Вот скрин, когда на полный экран, реализована возможность показывать только новые комментарии, а старые скрывать — экономит кучу времени на чтение обновлений. То есть. если например, для статьи в 300 комментариев завтра напишут только 2 или 3 новых комментария в разных местах, то их можно будет мгновенно прочитать, не тратя времени на перемещение по всем 300 комментариям.

      image
        –1
        Цель топика не написать расширение для Хабры.
        Основная идея была показать, как написать скрипт для X-Notifier.
          0
          Угу, понял.

          Да, x-notifier интересная вещичка.

          Просто ваши слова без учёта x-notifier мной были поняты так, что скрипта подобного нет и всё тут :-)

          Он позволяет получать уведомления о новых письмах, сообщениях, да о чем угодно с различных сервисов в одном месте. Для него написано множество скриптов (Gmail, Яндекс.Почта, Google+, Facebook, Twitter и прочих). Но скрипта для Хабра, до сих пор никто не написал, пора исправить это недоразумение!


          Наверное, было бы понятнее (если не вчитываться), если так:

          Он позволяет получать уведомления о новых письмах, сообщениях, да о чем угодно с различных сервисов в одном месте. Для x-notifier написано множество плагинов (Gmail, Яндекс.Почта, Google+, Facebook, Twitter и прочих), но для Хабра до сих пор никто не написал, пора исправить это недоразумение!
            0
            В программе скрипты называются именно скриптами для браузерного плагина/расширения x-notifier. Плагины для плагина как-то масло масляное получается
          0
          А где же версия для firefox? Вы сказали год назад, что рабоаете над над ней.
          0
          Сейчас поддерживаются браузеры Firefox и Chrome. Есть какие-то сложности с написанием скрипта под разные браузеры?
            0
            Я просто не тестировал в других браузерах, видимо разработчик X-Notifier тоже.
            Но, вот цитата с сайта плагина:
            Even though your browser is not in the list,
            you can use the script if you use one account for that script.

            Попробуйте, если будут баги, то поправлю.
            0
            Спасибо за инструкцию, благодаря ей написал скрипт для сайтов StackExchange.
            Только вот две вещи мне остались непонятны:
            1) можно ли как-то задавать параметры по умолчанию (например, чтоб обновление было не раз в 10 минут, а раз в час)?
            2) на что влияет возвращается ли true или false в результате работы каждого этапа? Почему в каких-то случаях следующий этап запускается вручную, а в каких-то — нет?
              +1
              Рад что кому то пригодилась эта писанина))
              1) Это конфигурируется в настройках плагина (Options -> Check interval), по умолчанию свое значение, кажется, задавать нельзя.
              2) Каждая стадия это инт. Когда мы возвращаем false, мы инкрементируем значение стадии на 1. К примеру тут в стадии ST_PRE_RES + 1 я делаю return false; и перехожу к следующей стадии ST_PRE_RES + 2. Если я не хочу переходить к следующей стадии, а сразу перейти к конкретному этапу то я присваиваю значение this.stage = ST_LOGIN; и возвращаю процесс. return true; это просто завершение.

              Я рекомендую посмотреть исходники плагина и можете посмотреть то что я понаписал, я думаю станет немного яснее.
              Если будут вопросы, то пишите, помогу)
                0
                Спасибо за пояснения! Касательно стадий — не сразу обратил внимание на дефолтные id стадий, оказывается ST_PRE и ST_PRE_RES как раз на единицу отличаются, потому в ST_PRE можно возвращать false.
                Свой первый скрипт я уже написал, вроде хорошо работает.

                Но вот над вторым скриптом все думаю. Хочу, чтобы X-Notifier уведомлял меня о новых отзывах на Google Play о моем приложении. API у Google Play нет, так что тут лишь парсить страницы выйдет. Но вот умеют ли скрипты X-Notifier хранить какие-то переменные до следующего запуска? Например, сохранить дату последнего отзыва, чтобы при следующей проверке определить появились ли новые отзывы или нет.
                  0
                  Можете использовать cookieManager, я его не использовал, но вы можете посмотреть примеры тут.
                    0
                    Похоже, cookieManager в последних версиях плагина X-Notifier отсутствует (или отсутствует в Chrome-версии плагина).
                    Кстати, обнаружил, что некоторые значения по умолчанию можно задавать в скрипте, например, var defaultInterval=120;.

                    Над вторым скриптом все бьюсь — научился получать данные об отзывах и извлекать их. Но вот с авторизацией в Google есть проблемы — взял за основу код от Gmail, авторизация срабатывает. Но вот потом надо как-то перейти на страницу с отзывами Google Play, а скрипт страницы GMail'a получает.
                      0
                      Можете показать код? Может смогу помочь.
                      Посмотрите так же что у вас стоит правильный параметр для service здесь
                      this.loginData=["https://accounts.google.com/ServiceLoginAuth?service=mail", "Email","Passwd","PersistentCookie=yes"];
              0
              Решил еще один скрипт написать и снова столкнулся с той же проблей — логика перехода между шагами (this.stage) по-прежнему не ясна. В каких-то случаях return false автоматически добавляет +1 к номеру шага. Но в каких-то шагах нет. Например, если в ST_LOGIN_RES вернуть false, то ST_LOGIN_RES+1 автоматически не запустится. Еще в каких-то случаях, видимо, запуск this.getHtml приводит к увеличению шага на 1 (наверное, когда завершается выполнение получения HTML-страницы). Но в каких-то случаях вообще используется return this.process(aData, aHttp), а в каких-то — return this.baseProcess(aData, aHttp).

              В получении данных мне нужно было сделать несколько getHtml запросов (без получения результата) и один getHtml для последующего использования в getCount. Но предсказать что в этом случае попадет как aData в getCount невозможно — судя по всему то, что завершится первым.

              Only users with full accounts can post comments. Log in, please.