Telegram бот для персоналізованої добірки статей з Хабра

Для запитань у стилі «навіщо?» є старіша стаття — Натуральний Geektimes - робимо простір чистішим.

статей багато, з суб'єктивних причин деякі не подобаються, а деякі, навпаки, шкода пропускати. Хочеться оптимізувати цей процес та економити час.

У вищезгаданій статті пропонувався підхід зі скриптами в браузері, але він мені не дуже сподобався (хоч я ним і користувався раніше) з наступних причин:

  • Для різних браузерів на комп'ютері/телефоні доводиться налаштовувати заново, якщо це взагалі можливо.
  • Жорстка фільтрація за авторами не завжди зручна.
  • Не вирішено проблему з авторами, чиї статті не хочеться пропускати, навіть якщо вони виходять щорічно.

Вбудована в сайт фільтрація за рейтингом статей не завжди зручна, тому що вузькоспеціалізовані статті за всієї їхньої цінності можуть отримувати досить скромний рейтинг.

Спочатку я хотів генерувати rss стрічку (або навіть декілька), залишаючи там тільки цікаве. Але в результаті вийшло, що читання rss здалося не дуже зручним: у будь-якому випадку для коментування/голосування за статтю/додавання її до обраного доводиться заходити через браузер. Тому я написав бота для телеграми, які кидає мені в личку цікаві статті. Телеграм сам по собі робить з них гарні превьюшки, що у поєднанні з інформацією про автора/рейтинг/перегляди виглядає досить інформативно.

Telegram бот для персоналізованої добірки статей з Хабра

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

Коротко про бот

Репозиторій: https://github.com/Kright/habrahabr_reader

Робот у телеграмі: https://t.me/HabraFilterBot

Користувач задає додатковий рейтинг для тегів та авторів. Після цього до статей застосовується фільтр — складаються рейтинг статті на Хабре, рейтинг користувача автора і середнє для рейтингів користувача за тегами. Якщо сума виявляється більшою від заданого користувачем порогового значення, то стаття проходить фільтр.

Побічною метою написання бота було отримання фана та досвіду. Крім того, я регулярно нагадував собі, що я - не гугл, а тому багато речей зроблено максимально просто і навіть примітивно. Втім, це не завадило процесу написання робота розтягнутися на три місяці.

За вікном було літо

Закінчувався липень, а вирішив написати бота. І не поодинці, а зі знайомим, який освоював scala і хотів щось написати на ній. Початок виглядав багатообіцяючим - код буде пилятися "комадною", завдання здавалося неважким і я думав, що через пару тижнів або місяць бот буде готовий.

Незважаючи на те, що я сам останні кілька років час від часу пишу код на скелі, цей код зазвичай ніхто не бачить і не дивиться: проекти, перевірка якихось ідей, передобробка даних, освоєння якихось концепцій з ФП. Мені було реально цікаво, як виглядає написання коду в команді, тому що код на скелі можна писати дуже по-різному.

Що ж могло піти так? Втім, не поспішатимемо події.
Все, що відбувається, можна відстежити з історії коммітів.

Знайомий створив репозитрій 27 липня, але більше нічого не зробив, а тому почав писати код.

30 липня

Коротко: я написав парсинг rss стрічки Хабра.

  • com.github.pureconfig для читання typesafe конфігів прямо в case класи (виявилося дуже зручно)
  • scala-xml для читання xml: оскільки спочатку я хотів написати свою реалізацію для rss – стрічки, а rss стрічка у форматі xml, то для парсингу використав цю бібліотечку. Власне, парсинг RSS теж з'явився.
  • scalatest для тестів Навіть для крихітних проектів написання тестів економить час - наприклад, при налагодженні парсингу xml набагато простіше завантажити його у файлик, написати тести та виправити помилки. Коли надалі з'явився баг із парсингом якихось дивних html із невалідними utf-8 символами, виявилося знову ж таки зручніше покласти його у файлик і додати тест.
  • актори з Akka. Об'єктивно, вони взагалі були потрібні, але проект писався for fun, хотів спробувати. В результаті готовий сказати, що мені сподобалося. На ідею ОВП можна поглянути з іншого боку — є актори, які обмінюються повідомленнями. Що цікавіше - можна (і потрібно) писати код з таким розрахунком, що повідомлення може не дійти або не бути оброблене (взагалі кажучи, при роботі аккі на одному комп'ютері повідомлення не повинні губитися). Я спочатку ламав голову і в коді відбувався треш з підписками акторів один на одного, але в результаті вдалося прийти доволі простої та витонченої архітектури. Код усередині кожного актора можна вважати однопоточним, при падіннях актора акка перезапускає його - виходить досить стійка до відмови система.

9 серпня

Я додав до проекту scala-scrapper для парсингу html сторінок з хабра (щоб витягувати інформацію типу рейтингу статті, кількості додавань до закладок тощо).

І Cats. Ті самі, що у скелі.

Telegram бот для персоналізованої добірки статей з Хабра

Я тоді читав одну книжку про розподілені бази даних, мені сподобалася ідея CRDT (Conflict-free replicated data type, https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type, хабр), тому я запилив тайп-клас комутативної напівгрупи для інформації про статтю на хабрі.

Насправді, ідея дуже проста — ми маємо лічильники, які монотонно змінюються. Кількість промотрів плавно зростає, кількість плюсів також (втім, як і кількість мінусів). Якщо у мене є дві версії інформації про статтю, то можна їх «злити в одну» — акутальнішою вважати той стан лічильника, який більший.

Напівгрупа означає, що два об'єкти з інформацією про статтю можна злити в один. Комутитивна позначає, що зливати можна і А+B та B+A, результат від порядку не залежить, у результаті залишиться найновіша версія. До речі, асоціативність тут також є.

Наприклад, за задумом, rss після парсингу давала трохи осліблену інформацію про статтю — без метрик типу переглядів. Спеціальний актор після цього брав інформацію про статті та бігав до html сторінок, щоб її оновити та злити зі старою версією.

Взагалі кажучи, як і в Akka, в цьому не було потреби, можна було просто для статті зберігати updateDate і брати новішу без будь-яких злиттів, але мене вела дорога пригод.

12 серпня

Я почав вільніше почуватися і заради інтересу зробив, щоб кожен чат був окремим актером. Теоретично, актер сам по собі важить близько 300 байт і їх можна хоч мільйонами створювати, тож це цілком нормальний підхід. Вийшло, як мені здається, досить цікаве рішення:

Один актор був мостом між сервером телеграма та системою повідомлень в акці. Він просто отримував повідомлення та відправляв їх потрібному акторові. Актор-чат у відповідь міг послати щось назад — і воно вирушало назад у телеграм. Що було дуже зручно — цей актор вийшов максимально простим та містив лише логіку відповіді на повідомлення. До речі, інформація про нові статті приходила в кожен чат, але я знову ж таки не бачу в цьому жодних проблем.

Загалом бот вже працював, відповідав на повідомлення, зберігав список відправлених користувачеві статей і я вже думав про те, що бот практично готовий. Я потихеньку допилював маленькі фішки типу нормалізації імен авторів та тегів (замінював "sd f" на "s_d_f").

Залишалося одне маленьке але - Стан нікуди не зберігався.

Все пішло не так

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

  • Для зберігання стану з'явилася mongoDB. Заодно в проекті зламалися логи, тому що монга навіщось починала в них спаміти і дехто їх просто глобально вимкнув.
  • Актор-міст у телеграм перетворився до невпізнанності і почав сам парсити повідомлення.
  • Актори для чатів були безжально випиляні, замість них з'явився актор, який ховав всю інформацію про всіх чатах відразу. На кожен чих цей актор ліз у монгу. Ну так, типу при оновленні інформації про статтю відправити її всім акторам-чатам – важко (ми ж як гугл, мільйони користувачів так і чекають по мільйону статей у чат для кожного), а от при кожному оновленні чату лізти в монгу – це нормально. Як я зрозумів значно пізніше, працююча логіка роботи чатів теж була повністю випиляна і натомість з'явилося щось непрацююче.
  • Від тайп-класів не залишилося й сліду.
  • В акторах з'явилася якась хвора логіка з підписками їх один на одного, що веде до ступеня умови.
  • Структури даних із полями типу Option[Int] перетворилися на Int з магічними дефолтними значеннями типу -1. Пізніше я зрозумів, що mongoDB зберігає json та немає нічого поганого у тому, щоб зберігати там Option ну чи хоча б парсить -1 як None, але на той момент я цього не знав і повірив на слово, що так треба. Той код писав не я, і я не ліз його змінювати до певного часу.
  • Я дізнався, що моя публічна адреса айпи має властивість змінюватися, і щоразу доводилося додавати його в whitelist монге. Бота я запускав локально, монга була десь на серверах монги як компанії.
  • Раптом зникла нормалізація тегів та форматування повідомлень для телеграм. (Хм, з чого б це?)
  • Мені сподобалося, що стан робота зберігається у зовнішній БД, і при перезапуску він продовжує працювати як ні в чому не бувало. Втім, це був єдиний плюс.

Друга людина не особливо поспішала, і всі ці зміни з'явилися однією великою купою вже на початку вересня. Я відразу оцінив масштаб отриманих руйнувань і почав розбиратися у роботі БД, т.к. раніше і з ними не мав справу. Тільки потім я зрозумів, скільки працюючого коду було випиляно і скільки багів додано натомість.

Вересень

Спочатку я думав, що корисно було б освоїти монгу і зробити все добре. Потім я потихеньку почав розуміти, що організувати спілкування з бд теж мистецтво, в якому можна наробити гонок і просто помилок. Наприклад, якщо від користувача надійдуть два повідомлення типу /subscribe - і ми у відповідь на кожне створимо за записом у табличці, тому що на момент обробки тих повідомлень користувач не підписаний. У мене виникла підозра, що спілкування з монгою в існуючому вигляді написано не найкращим чином. Наприклад, налаштування користувача створювалися тоді, коли він підписувався. Якщо він намагався їх змінити до факту підписки ... робот нічого не відповідав, тому що код в акторе ліз у основу за налаштуваннями, не шукав і падав. На запитання — чому б не створювати налаштування за потребою я дізнався, що нічого їх змінювати, якщо користувач не підписався... там помилка.

Списку відправлених до чату статей не було, натомість було запропоновано, щоб я сам їх написав. Мене це здивувало — я загалом був не проти втягування в проект всяких штук, але було б логічно втягти ці штуки їх і прикрутити. Але ні, другий учасник, схоже, підзабив на все, але сказав, що список усередині чату — нібито погане рішення, і треба зробити табличку з івентами типу «користувачеві x була надіслана стаття у». Потім, якщо користувач запитував надіслати нові статті, треба було відправити запит до бд, який з івентів виділив би івенти, що стосуються користувача, ще отримати список нових статей, відфільтрувати їх, відправити користувачеві і накидати івентів назад у бд.

Другого учасника кудись понесло у бік абстракцій, коли боту приходитимуть не лише статті з Хабра та вирушатимуть не лише до телеграм.

Я якось реалізував івенти у вигляді окремої таблички до другої половини вересня. Неоптимально, але бот хоча б заробив і знову почав надсилати мені статті, а я потихеньку розібрався з тим, що відбувається в коді.

Тепер можна повернутися спочатку і згадати, що репозиторій спочатку створював не я. Що ж могло так піти? Мій пул-реквест був відхилений. Виявилося, що в мене бидлокод, що я не вмію працювати в команді і я повинен був правити баги в поточній кривій реалізації, а не доопрацьовувати її до стану юзабельного.

Я засмутився, переглянув історію коммітів, кількість написаного коду. Подивився на моменти, які спочатку були добре написані, а потім зламані назад…

F*rk it

Я згадав статтю Ви - не Google.

Подумав, що ідея без реалізації нікому особливо не потрібна. Подумав про те, що я хочу мати працюючого робота, який буде в одному-єдиному екземплярі працювати на одному-єдиному комп'ютері як проста java-програма. Я знаю, що мій бот працюватиме місяцями без перезапусків, добре в минулому я вже писав таких роботів. Якщо він раптом все-таки впаде і не надішле користувачеві чергову статтю, небо не обрушиться на землю і нічого катастрофічного не станеться.

Навіщо мені докер, mongoDB та інший карго-культ «серйозного» софту, якщо код тупо не працює чи працює криво?

Я форкнув проект і зробив усе, як хотів.

Telegram бот для персоналізованої добірки статей з Хабра

Приблизно тоді я змінив місце роботи і вільного часу стало катастрофічно не вистачати. Вранці я прокидався рівно на електричку, увечері повертався пізно і щось робити вже не хотілося. Я якийсь час не робив нічого, потім бажання дописати бота пересилило, і я став потихеньку переписувати код, поки їздив на роботу вранці. Не скажу, що це було продуктивно: сидіти в електричці, що тремтить, з ноутбуком на колінах і підглядати на stack overflow з телефону не дуже зручно. Втім, час за написанням коду пролітав непомітно, і проект почав потихеньку рухатися до робочого стану.

Десь у глибині душі був черв'ячок сумніву, який хотів використати mongoDB, але я подумав, що крім плюсів із «надійним» зберіганням стану є помітні мінуси:

  • БД стає ще однією точкою відмови.
  • Код стає складнішим, і писати його я буду довше.
  • Код стає повільним і неефективним, замість зміни об'єкта пам'яті зміни відправляються в БД і за необхідності витягуються назад.
  • З'являються обмеження типу зберігання івентів в окремій табличці, пов'язані з особливостями роботи БД.
  • У тріальній версії монги є якісь обмеження, і якщо в них упертись, доведеться на чомусь монгу запускати та налаштовувати.

Випилив монгу, тепер стан робота просто зберігається в пам'яті програми і іноді зберігається у файлик у вигляді json. Можливо, в коментарях напишуть, що я не прав, бд саме тут треба використовувати і т.п. Але це мій проект, підхід із файлом максимально простий і він працює прозорим чином.

Викинув магічні значення типу -1 і повернув нормальні Option, Додав зберігання хеш-таблички з відправленими статтями назад в об'єкт з інформацією про чаті. Додав видалення інформації про статті старше п'яти днів, щоб не зберігати все поспіль. Привів логування до робочого стану - логи в розумних кількостях пишуться і файл і консоль. Додав кілька адмінських команд типу збереження стану чи отримання статистики типу кількості користувачів та статей.

Виправив купу дрібниць: наприклад, для статей тепер вказується кількість переглядів, лайків-дизлайків та коментарів на момент проходження фільтра користувача. Взагалі, дивно, скільки дрібниць довелося виправити. Я вів списочок, відзначав там усі «шорсткості» і в міру можливостей виправляв їх.

Наприклад, я додав можливість прямо в одному повідомленні задати всі налаштування:

/subscribe
/rating +20
/author a -30
/author s -20
/author p +9000
/tag scala 20
/tag akka 50

І ще команда /settings виводить їх саме в такому вигляді, можна брати текст від неї та відправляти всі налаштування другові.
Наче дрібниця, але подібних нюансів — десятки.

Реалізував фільтрацію статей у вигляді простої лінійної моделі - користувач може задати додатковий рейтинг авторам і тегам, а також граничне значення. Якщо сума рейтингу автора, середнього рейтингу для тегів та реального рейтингу статті виявиться більшою за порогове значення, то стаття показується користувачеві. Можна або просити у робота команди /new, або підписатися на робота і він буде кидати статті в особу в будь-який час доби.

Взагалі кажучи, у мене була ідея для кожної статті витягнути більше ознак (хаби, кількість коментарів, додавань в закладки, динаміку зміни рейтингу, кількість тексту, картинок та коду у статті, ключові слова), а користувачеві показувати голосування ок/не ок під кожною статтею і під кожного користувача навчати модель, але мені стало ліньки.

До того ж, логіка роботи стане не такою очевидною. Зараз я можу вручну поставити для patientZero рейтинг +9000 і при пороговому рейтингу в +20 гарантовано отримуватиму всі його статті (якщо, звичайно, не поставлю -100500 для яких-небудь тегів).

Підсумкова архітектура вийшла досить простою:

  1. Актор, який зберігає стан усіх чатів та статей. Він вантажить свій стан з файлика на диску і іноді зберігає його назад, щоразу в новий файл.
  2. Актор, який час від часу набігає в rss-стрічку, дізнається про нові статті, заглядає за посиланнями, парсить, і оправляє ці статті першому акторові. Крім того, він іноді запитує у першого актора список статей, вибирає ті з них, які не старші трьох днів, але при цьому давно не оновлювалися, і оновлює їх.
  3. Актор, який спілкується із телеграмом. Я таки виніс парсинг повідомлень повністю сюди. По-хорошому хочеться розділити його на два — щоб один паршив вхідні повідомлення, а другий займався транспортними проблемами типу перенаправлення повідомлень, що не відправилися. Зараз перенаправлення немає, і повідомлення, що не дійшло через помилку, просто загубиться (хіба що в логах відзначиться), але поки що це не викликає проблем. Можливо, проблеми виникнуть, якщо на бота підпишеться купа чоловік і я досягну ліміту на відправлення повідомлень).

Що мені сподобалося - завдяки akka падіння акторів 2 і 3 взагалі не впливають на працездатність бота. Можливо, якісь статті не оновлюються вчасно або якісь повідомлення не доходять до телеграми, але акка перезапускає актор і все продовжує працювати далі. Я зберігаю інформацію про те, що стаття показана користувачеві тільки тоді, коли телеграма актора відповість, що він успішно доставив повідомлення. Найстрашніше, що мені загрожує відправити повідомлення кілька разів (якщо воно доставиться, але твердження якимось невідомим чином загубиться). У принципі, якби перший актор не зберігав стан у собі, а спілкувався з якоюсь бд, він міг би теж непомітно падати і повертатися до життя. Ще я міг би спробувати akka persistance для відновлення стану акторів, але поточна реалізація влаштовує мене своєю простотою. Не те щоб мій код часто падав — навпаки, я доклав чимало зусиль, щоб це було неможливо. Але shit happens, і можливість розбити програму на ізольовані шматочки-актори здалася мені реально зручною та практичною.

Додав circle-ci для того, щоб при поломці коду відразу про це дізнаватися. Як мінімум про те, що код перестав компілюватися. Спочатку хотів додати travis, але він показував лише мої проекти без форкнутих. Загалом обидві ці штуки можна вільно використовувати на відкритих репозиторіях.

Підсумки

Вже листопад. Бот написаний, я користувався ним останні два тижні, і мені сподобалося. Якщо є ідеї щодо покращення — пишіть. Я не бачу змила монетизувати його — нехай просто працює та надсилає цікаві статті.

Посилання на бота: https://t.me/HabraFilterBot
Гітхаб: https://github.com/Kright/habrahabr_reader

Невеликі висновки:

  • Навіть маленький проект може сильно затягтися за часом.
  • Ви не гугл. Немає сенсу стріляти з гармати горобцями. Просте рішення може працювати анітрохи не гірше.
  • Пет-проекти дуже добре підходять для експериментів із новими технологіями.
  • Телеграм роботи пишуться досить просто. Якби не «командна робота» та експерименти з технологіями, бот був би написаний за тиждень-два.
  • Модель акторів - цікава штука, що добре поєднується з багатопоточністю і стійкістю до відмови коду.
  • Здається, я відчув на собі, чому open source співтовариство любить форки.
  • Бази даних хороші тим, що стан програми перестає залежати від падіння/перезапуску програми, але робота з БД ускладнює код і накладає обмеження на структуру даних.

Джерело: habr.com

Додати коментар або відгук