Pull to refresh

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

Reading time 7 min
Views 9.4K

Есть хороший плагин для всех популярных браузеров, 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. Форки и пулл реквесты приветствуются.
Страница дополнения.
Tags:
Hubs:
+16
Comments 17
Comments Comments 17

Articles