Автоматизированное получение золотого значка на StackOverflow c помощью Nightmare

Изучая значки (badges) на StackOveflow.com, я обратил внимание на золотую награду "Фанатик" (Fanatic), которая дается при выполнении условия:

Visit the site each day for 100 consecutive days. (Days are counted in UTC.)

(пер.: "Посещать сайт ежедневно в течение 100 дней подряд. (Учёт дней по UTC.)"

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

Задача скрипта – авторизоваться на сайте, зайти в профиль пользователя и получить текущий статус (сколько дней учтено).
Для просмотра прогресса я установил fanatic badge в качестве отслеживаемого.

Stackoverflow badge status

Приведен пример отображения статуса для значка "Civic Duty".
Автор cделал данный скриншот уже после получения "Fanatic".

Полученный статус отправляется на почту, дабы наблюдать за выполнением.

Все это запускается по Cron'у ежедневно, на виртуальном сервере.

Привожу некоторые выдержки из кода.

SO Visitor

За основу был взят фреймворк Nightmare, используемый для написания e2e-тестов.

index.js, part 1:

const Nightmare = require('nightmare');
const nightmare = Nightmare({show: false});

nightmare
    .goto('https://stackoverflow.com/users/login')
    .wait('#login-form')
    .type('#email', 'email@example.com')
    .type('#password', '123456')
    .click('#submit-button')
    .wait('a.my-profile')
    .click('a.my-profile')
    .wait('#top-cards')
    .evaluate(() => {
        // Извлекаем текущий прогресс
        const el = document.querySelector('#top-cards span.-count');
        return el ? el.innerText : 'null';
    })
    .end()
    .then(progressText => {
        // Получили статус строку вида: "31/100".
        processResult(progressText);
    })
    .catch(function (error) {
        processError(error);
    });

Email

Для отправки уведомлений о прогрессе я воспользовался адаптером mailgun-js для одноименного сервиса по рассылке почты mailgun.com.

index.js, part 2:

const Mailgun = require('mailgun-js');
const mailgun = Mailgun({
        apiKey: "MY_API_KEY",
        domain: "my-website.com"
    });

mailgun
    .messages()
    .send({
        from: 'SO Visitor <no-reply@mailgun.org>',
        to: confFile.email,
        subject: `Stackoverflow visiting report (${text})`,
        text: `Hi man. Thats your stat for now: ${text}`
    }, (error, body) => {
        if (error) {
            throw error;
        }
    });

Headless

Несмотря на то, что инстанс nightmare можно запустить без отображения на экране (show:false), для запуска на машине без графической подсистемы потребуется Xvfb (X virtual framebuffer).

Кстати говоря, на моем продакшн-сервере используется достаточно старая версия Node.js, без поддержки ES6. Поэтому задача, запускаемая кроном, обзавелась строчками для указания альтеранитивной версии интерпретатора node (v6+, используя NVM).

Входная точка, скрипт который вызывает cron:

#!/bin/bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  && nvm use 6  > /dev/null

# Запуск через Xvfb
xvfb-run node /home/deliaz/so_visitor/index.js

Code

Полный и отрефакторенный код проекта можно получить в репозитории so_visiter.

Achieved

Статья написана после успешного получения значка.
За время работы скрипта проблема возникала лишь однажды: StackOverflow изменил имя у класса счетчика на странице профиля. Исправил коротким коммитом.