Когда у сервиса нет публичного API, но есть почтовая рассылка – Mikulski
Наложение сайта

Когда у сервиса нет публичного API, но есть почтовая рассылка

ДИСКЛЕЙМЕР:
Я не программист, а лишь энтузиаст-копипастер, который делится тем, в чем смог разобраться и добиться работоспособности. Поэтому, не исключено, что знающие специалисты некоторые моменты или формулировки могут счесть неправильными или нелепыми.
Подача данного материала подразумевает, что вы в некоторой степени владеете основами node.js.
Я использую связку node.js и pm2-manager.

Не так давно я добавил отображение последних событий на стриме для свой 24/7 трансляции: подписки, рейды и т.д. События я цепляю разными способами: часть через официальные API Twitch, VK и Telegram, часть через веб-сокеты Donation Alerts и Streamlabs. Для Kick, например, более экзотичная техника: с помощью библиотеки KickChatConnection подключаюсь к чату своего канала (без возможности туда писать) и отбираю сообщения по ключевым фразам. Например, когда Botrix пишет свою реакцию на событие, я режу это сообщение, чтобы выудить никнейм:

if (data.includes("Thank you for the follow @") && jsonDataSub.sender.username === 'BotRix'){
console.log(jsonDataSub.content.match(/\w+$/)[0] + " is now following Kick")
   }
if (data.includes("Thank you for the raid @") && jsonDataSub.sender.username === 'BotRix'){ 
console.log(jsonDataSub.content.match(/\w+$/)[0] + " Kick Raid")
   }

Немного странно, зато работает 🙂

Но вот дело дошло до Boosty. Окей, подписки, платные подписки и их продление замечательно приходят через сокет Donation Alerts. Единственное чего не хватает, так это донатов, отправляемых через страницу на Boosty.
Конечно, есть умельцы, которым подвластна техника обратной инженерии, чтобы реализовывать подключение напрямую к API-вебсайта, но, понятное дело, я не из их числа. В общем, не сумев найти какого-либо примера по использованию неофициального API Boosty, который бы для меня сработал, я отправился спать.
Лежу и думаю, а как все-таки доставать-то эти донатные события? И тут понимаю, что их Telegram-бот присылает уведомления, а также они приходят в почтовой рассылке. Сон как рукой сняло, а я пошел гуглить, можно ли как-то парсить личные сообщения, приходящие от бота в Telegram. С Telegram это было бы наиболее просто и удобно, но ничего по этой теме найти не удалось. Вероятно, это невозможно из соображений безопасности. А вот реализовать такой подход через почту через IMAP-протокол оказалось вполне реальным.

Почтовый ящик

Не будучи уверенным в том, насколько это вообще безопасно – парсить письма на удаленный сервер, я решил, что стоит под это дело стоит завести отдельный ящик. На который не приходят сообщения с “чувствительной” информацией. Благо Boosty, именно, для уведомлений позволяет привязать любую почту и которая не используется для логина в аккаунт. Но, возможно, что это излишняя предосторожность, так как подключение происходит через зашифрованный шлюз на порте 993 с tls.
Такой почтовый ящик я завел на Яндекс.
Чтобы для него включить передачу писем по протоколу IMAP, нужно перейти в настройки почты (иконка с шестеренкой), затем:
все настройки -> почтовые программы -> поставить галочку в чекбоксе "Разрешить доступ к почтовому ящику с помощью почтовых клиентов: С сервера imap.yandex.ru по протоколу IMAP".

По требованиям сервиса, нужно сгенерировать пароль приложения. Для этого нужно перейти на вкладку “Безопасность” Яндекс-аккаунта: https://id.yandex.ru/security (Управление аккаунтом -> Безопасность), спуститься вниз страницы и перейти на вкладку "Пароли приложений" -> Создать пароль приложения -> Почта.
Тут попросят задать название для пароля (не пароль, а его наименование, например, imap-boosty), после чего сгенерируется пароль. Он показывается всего один раз, до закрытия вкладки, поэтому его надо скопировать и сохранить.

node-imap

Для подключения к ящику, я пользуюсь библиотекой imap, а для парсинга текста сообщений mailparser. Документация imap не очень нуб-дружелюбная, но разобраться можно: там достаточно много самых разных опций для манипуляций с почтой.

npm i imap
npm i mailparser

С настройками по умолчанию и теми, что даются в примерах документации, сокет подключения слишком часто обрывается. Скрипт, конечно, все равно автоматически восстановит коннект, но все же стоит подшаманить с настройками keepalive, чтобы сократить количество дисконнектов. У меня это выглядит примерно вот так:

var fs = require('fs'), fileStream;
const Imap = require("imap");
const { inspect } = require("util");
const simpleParser = require('mailparser').simpleParser;

const imapServer = new Imap({
  user: "почтовый_ящик@yandex.ru",
  password: "сгенерированный_пароль_приложения",
  host: "imap.yandex.ru",
  port: 993,
  tls: true,
  keepalive: {
            interval: 5000,
            idleInterval: 120000,
            forceNoop: true
        }
});

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

imapServer.once('ready', function() {
imapServer.openBox('INBOX', false, function (err, box) {
if (err) throw err;
   
imapServer.search(['UNSEEN'], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
console.log("Есть непрочитанные сообщения!")
    }
    });
  });
});

imapServer.once("error", (err) => {
  console.log(err);
});
imapServer.once("end", () => {
  console.log("Connection ended");
});
imapServer.connect(console.log('imap connected'));

Эта проверка осуществляется лишь разово (тэг ‘ready’) при первом подключении и нужна, чтобы открыть ящик со входящими.
Чтобы получать реакцию на новые непрочитанные сообщения используется тэг ‘mail’:

imapServer.on('mail', function() {
imapServer.search(['UNSEEN'], async (err, results) => {         
if (err) throw err;
if (results && results.length > 0) {
console.log("Новое письмо!")
    }
  });
})

Хорошо, теперь, чтобы выуживать письма с нужным заголовком от нужного адресата и отмечать их прочитанными надо переписать условия поиска (на примере уведомления о донате на Boosty):

imapServer.on('mail', function() {
imapServer.search(['UNSEEN', ['FROM', "noreply@boosty.to"], ['HEADER', 'SUBJECT' ,"You have a new donation"]], async (err, results) => {    
if (err) throw err;
if (results && results.length > 0) {
var f = imapServer.fetch(results, {
bodies: '',
markSeen: true      
});
console.log('Донат на Boosty!')
}
});
});

Целиком:

var fs = require('fs'), fileStream;
const Imap = require("imap");
const { inspect } = require("util");
const simpleParser = require('mailparser').simpleParser;

const imapServer = new Imap({
  user: "почтовый_ящик@yandex.ru",
  password: "сгенерированный_пароль_приложения",
  host: "imap.yandex.ru",
  port: 993,
  tls: true,
  keepalive: {
            interval: 5000,
            idleInterval: 120000,
            forceNoop: true
        }
});

imapServer.once('ready', function() {
imapServer.openBox('INBOX', false, function (err, box) {
if (err) throw err;
   
imapServer.search(['UNSEEN'], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
console.log("Есть непрочитанные сообщения!")
    }
    });
  });
});

imapServer.on('mail', function() {
imapServer.search(['UNSEEN', ['FROM', "noreply@boosty.to"], ['HEADER', 'SUBJECT' ,"You have a new donation"]], async (err, results) => {    
if (err) throw err;
if (results && results.length > 0) {
var f = imapServer.fetch(results, {
bodies: '',
markSeen: true      
});
console.log('Донат на Boosty!')
};
});
});

imapServer.once("error", (err) => {
  console.log(err);
});
imapServer.once("end", () => {
  console.log("Connection ended");
});
imapServer.connect(console.log('imap connected'));

В целом, этого уже достаточно, чтобы получить триггер для дальнейших манипуляций.
Тем не менее, можно прочесть содержимое письма и попробовать вытащить из него нужную информацию: пользователь и сумма.
Я это сделал через split-метод. Понятно, что в этом случае, все будет зависеть от верстки письма. Если она со временем изменится, то “вырезку” нужных данных придется переделывать заново. Впрочем, думаю, что с самого начала этой затеи ясно, что весь этот костыль – компромиссный и крайний вариант.
А еще с заголовком “You have a new donation” приходят разные по своему назначению донаты: просто донат, донат на цель, донат под постом. И у всех них разная верстка, следовательно, параметры split для всех будут разными.
Я покажу пример доната на цель:

imapServer.search(['UNSEEN', ['FROM', "noreply@boosty.to"], ['HEADER', 'SUBJECT' ,"You have a new donation"]], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
var f = imapServer.fetch(results, {
bodies: '',
markSeen: true
})
f.on('message', function(msg, seqno) {
msg.on('body', function(stream, info) {  
simpleParser(stream, (err, mail) => {
if (mail.text.includes('You have a new donation for the goal')) {
let username = mail.text.split(" ", 8)[7].slice(5);
let amount = mail.text.split(" ", 18)[17] + " RUB"
console.log(`${username} донатит ${amount} на Boosty!`);
      };
    });
   });     
  });
 };
});  

Так как парсинг тела сообщения занимает некоторое время (оно разнится от случая к случаю), то имеет смысл добавить таймаут в несколько секунд, прежде чем производить дальнейшие действия с полученными данными. Например, для записи в файл:

imapServer.search(['UNSEEN', ['FROM', "noreply@boosty.to"], ['HEADER', 'SUBJECT' ,"You have a new donation"]], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
var f = imapServer.fetch(results, {
bodies: '',
markSeen: true
})
f.on('message', function(msg, seqno) {
msg.on('body', function(stream, info) {  
simpleParser(stream, (err, mail) => {
if (mail.text.includes('You have a new donation for the goal')) {
let username = mail.text.split(" ", 8)[7].slice(5);
let amount = mail.text.split(" ", 18)[17] + " RUB"
console.log(`${username} донатит ${amount} на Boosty!`);

setTimeout(wait, 3000)
fs.writeFile('/путь/к/файлу/', `${username} донатит ${amount} на Boosty!`, function(err) {if (err) return console.log(err)});       
  
function wait() {
process.exit(0);     
}
      };
    });
   });     
  });
 };
});   

Еще после выполнения данной операции я добавил process.exit(0), чтобы скрипт прекращал свою работу и автоматически перезапускался через pm2. Так как на раннем этапе тестирования заметил, что скрипт через какое-то количество попыток прекращал реагировать на письма с одинаковым заголовком. Поэтому решил подстраховаться таким образом.

В конечном итоге весь этот костыль выглядит примерно так (этот код я не тестировал, если что не обессудьте):

var fs = require('fs'), fileStream;
const Imap = require("imap");
const { inspect } = require("util");
const simpleParser = require('mailparser').simpleParser;

const imapServer = new Imap({
  user: "почтовый_ящик@yandex.ru",
  password: "сгенерированный_пароль_приложения",
  host: "imap.yandex.ru",
  port: 993,
  tls: true,
  keepalive: {
            interval: 5000,
            idleInterval: 120000,
            forceNoop: true
        }
});

imapServer.once('ready', function() {
imapServer.openBox('INBOX', false, function (err, box) {
if (err) throw err;
   
imapServer.search(['UNSEEN'], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
console.log("Есть непрочитанные сообщения!")
    }
    });
  });
});

imapServer.search(['UNSEEN', ['FROM', "noreply@boosty.to"], ['HEADER', 'SUBJECT' ,"You have a new donation"]], async (err, results) => {
if (err) throw err;
if (results && results.length > 0) {
var f = imapServer.fetch(results, {
bodies: '',
markSeen: true
})
f.on('message', function(msg, seqno) {
msg.on('body', function(stream, info) {  
simpleParser(stream, (err, mail) => {
if (mail.text.includes('You have a new donation for the goal')) {
let username = mail.text.split(" ", 8)[7].slice(5);
let amount = mail.text.split(" ", 18)[17] + " RUB"
console.log(`${username} донатит ${amount} на Boosty!`);

setTimeout(wait, 3000)
fs.writeFile('/путь/к/файлу/', `${username} донатит ${amount} на Boosty!`, function(err) {if (err) return console.log(err)});       
  
function wait() {
process.exit(0);     
}
      };
    });
   });     
  });
 };
});     


imapServer.once("error", (err) => {
  console.log(err);
});
imapServer.once("end", () => {
  console.log("Connection ended");
});
imapServer.connect(console.log('imap connected'));

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

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии