Рил соушел ток, Антихайп

Или как мы сделали свой социальный редактор

Meduza
Meduza
Nov 24, 2017 · 9 min read

Часть первая. Максимально понятная

Социальные сети — один из основных каналов, через который люди находят и читают материалы «Медузы».

Год назад мы поняли, что нас не устраивает ситуация с социальными сетями. Это очень больно — писать текст «подводок» в трех разных интерфейсах (фейсбук, «ВКонтакте» и твиттер), следить за тем, чтобы посты не каннибализировали друг друга (например, в случае с фейсбуком и его умной лентой рич одного поста может сильно упасть, если другой пост был опубликован в то же время), делать так называемые отложенные посты. Ситуация становится хуже, когда наступает кризис (теракт или что-то похожее): приходится бежать в социальные сети и убирать старые отложенные посты, срочно публиковать более срочные посты про актуальное событие. Загрузка видео в соцсети — отдельная боль, тут к неудобству интерфейса добавляется долгое время загрузки, а ведь загрузить видео нужно в каждой из трех соцсетей по-отдельности, а для публикации на сайте еще и в Ютуб.

Изначально мы думали над чем-то вроде календаря, где редактор бы выбирал время и писал подводку к материалу. Но тут была проблема — непонятно, как в такой системе менять план на лету (например, когда теракт или что-то еще из ряда вон выходящее). В общем, идеи не было — не было и решения.

Пока не было идеи, мы пробовали сторонние сервисы. Этим летом, например, мы подключили сервис , который через пару недель тестирования случайно опубликовал наш пост в твиттере «Дождя». В итоге мы от него отказались. Но это заставило нас ускориться.

И идея появилась — у нашего бэкенд-разработчика и заместителя технического директора Бори Горячева. Так как мы все в «Медузе» в большей или меньшей степени фанаты Trello, то решили своровать идею доски и немного подкрутить ее под наши нужды. Фронтенд-разработчик Никита Комарков придумал название для редактора — «Антихайп».

Итак, каждый столбец — платформа (группа «ВКонтакте», например). У каждого столбца есть таймер: 5, 10, 30, 60, 120 минут. В зависимости от новостного потока и платформы, редактор выбирает нужный ему таймер. Много новостей — 30 минут в фейсбуке, 10 минут в твиттере. Выходные — по часу везде. Каждая платформа может быть независимо от другой очищена — если произошло очень важное событие и вся редакция работает по одной теме. Каждая платформа может встать на паузу — удобно, когда в спокойном режиме хочется собрать постов на ночь.

Каждая карточка (по терминологии Trello) или сниппет (по нашей) — это статус + ссылка на материал. Плюс динамическое время публикации, которое рассчитывается в зависимости от таймера платформы, даты последней публикации и количества сниппетов стоящих перед этой карточкой. Когда редактор собирает сниппеты для платформы, он тянет их из результатов поиска, пишет или редактирует статус и ставит в очередь. Когда наступает время, сниппет отправляется в социальную сеть.

Любой редактор материала может написать статус для своего материала сам в редакторе материала. И тогда при заведении статьи в столбец «Антихайпа» там сразу появится уже написанный статус.

Так выглядит редактирование сниппета:

Так это работает:

Примерно сразу после запуска мы поняли, что не продумали сценарий работы в конце дня. В этом сценарии у редактора еще есть 2–3 текущих материала для социальных сетей, но ему уже пора планировать отложенные посты на ночь. Эти 2–3 сообщения должны выйти с интервалом в 30 минут, а ночные выходят раз в час. Мы решили эту проблему тем, что ввели «пустой» сниппет, который назвали не очень понятным словом «Новые интервалы» — если поставить его в очередь, в нужный момент интервал платформы переключится. Это позволяет более гибко планировать публикации.

Так это выглядит:

Отдельная важная вещь — публикация видеоматериалов, которых на «Медузе» становится все больше и больше. Раньше видеоотдел публиковал все ролики руками во все социальные сети — это очень трудоемкий процесс (иногда заливка одного видео может занять до 40 минут). Мы это исправили, причем исправили одновременно с запуском «Антихайпа». Вряд ли это было правильно (пришлось разом выпускать два сложнейших проекта), но сделали это мы не просто так: видеоредакторы публикуют видео сразу в соцсети. Видео у нас много, а через «Антихайп» на данный момент можно публиковать только материалы. При этом «Антихайп» не знает о том, что выложено, например, в фейсбук напрямую. И если бы видеоредакторы продолжили выкладывать по 5 видео в день по старинке, очередь в «Антихайпе» не соответствовала бы реальности.

Поэтому мы сделали редактор видео. Выглядит он так:

Видео загружается в «Монитор» (так называется наша админка), который на его основе создает непубличное видео в ютубе. Если такой материал вывесить через «Антихайп» в фейсбук или «ВКонтакте», в социальной сети появится не ссылка на материал, а само видео. Это крайне упрощенное описание видеоредактора, на самом деле при разработке мы столкнулись с полным адом — каждая из поддерживаемых нами соцсетей работает с видео не так, как другие. Нет, правда, это ад.

Аналитика

Мы стараемся максимально подробно измерять то, откуда читают «Медузу». Интересно, какая часть трафика из соцсетей приходит через посты в наших официальных аккаунтах, а какая — через собственные посты читателей. И если раньше utm-метки к ссылкам приходилось добавлять вручную, то теперь это автоматизировано: «Антихайп» сам дописывает метки к публикуемым ссылкам, и мы можем полнее оценить эффективность отдельных аккаунтов.

Впечатления редакции

«Антихайп» в разы упростил работу с социальными сетями: если раньше приходилось отдельно заходить на сайт каждой из них, то теперь все делается в одной вкладке внутри «Монитора». Но самая большая магия начинается, когда нужно работать с запланированными на будущее постами. Так как мы стараемся не валить в соцсети все подряд, а расставлять посты с более-менее равными промежутками, то раньше постоянно приходилось добавлять таймеры к публикациям. Это было половиной беды — а вот когда возникала необходимость что-то подвинуть, чтобы поставить более срочный материал, начиналась настоящая боль.

С «Антихайпом» все это стало намного проще: ты передвигаешь карточку с нужным материалом выше или ниже в очереди, а система сама пересчитывает время выхода и публикует, когда нужно. Никакой возни с таймерами и расчетами, на какое время какой пост нужно запланировать.

В теории мы могли воспользоваться каким-то сторонним решением, но безусловное преимущество «Антихайпа» — тесная интеграция с «Монитором». Система видит даже материалы, которых нет на сайте, но которые были созданы специально для соцсетей. Ну и список платформ мы контролируем сами: не нужен LinkedIn — нет LinkedIn, понадобился Telegram — добавили Telegram.

Это была первая часть рассказа — в меру понятная. Сейчас будет вторая часть — совсем непонятная. В ней Боря расскажет, как это устроено внутри.

Часть вторая. Боря рассказывает, как это устроено внутри

У нас есть приложение в котором редакторы пишут материалы, формируют главную и вообще работают. Это приложение называется «Монитор» — тут можно почитать про него подробнее. Этому проекту уже три года, оно написано на ruby on rails. Когда я думал о том, как писать «Антихайп», то понял, что у меня нет никого желания писать его на ruby. Поймите меня правильно: ruby on rails — отличная штука, но такие вещи, как параллельная работа, отложенные вычисления и что, наверное, самое важное, вебсокеты — не самые ее сильные стороны (да, я в курсе про action cable, но что-то не хочется). И… так как мы любим микросервисы, я решил, что пусть это будет elixir + phoenix framework. Я решил, что:

  • Пусть этот сервис работает с той же базой данных, что и «Монитор»
  • Пусть у него будет 1 эндпоинт для вебсокет-соединения
  • Пусть его фронтэнд будет в коде «Монитора» (react)
  • Пусть он будет запускаться отдельно от «Монитора». Деплой «Монитора» не влияет на работу «Антихайпа». В итоге «Антихайп» с точки зрения фронтэнда — 1 адрес для wss-соединения.

Модели
Конечно, встал вопрос: где делать миграции (база-то одна)? Я выбрал rails, тут нет какого-то плюса или минуса. Просто это показалось проще. На стороне phoenix есть два контекста — Monitor и Social. Monitor ответственен за те части, которые нужны из основного приложения: это схема post (та таблица в которой лежат все материалы «Монитора») и user (ну пользователи «Монитора»). Social-контекст состоит из двух схем — platform и snippet

Platform выглядит так

platform.ex
snippet.ex

Сниппеты упорядочены по ord, имеют статусы — pending, sent, deleted, sending. Они принадлежат пользователю (id редактора, который последним редактировал сниппет) и у них есть body — собственно, текст сообщения, которое уйдет в социальную сеть. Ссылка на материал забирается из Monitor.Post.

Еще у сниппета есть meta — это json, в который, в зависимости от того, куда отправляется сниппет, пишется идентификатор от социальной сети и ссылка на пост в сети.

Таймеры
Наверное, самая сложная часть этого проекта — процессы, которые занимаются отправкой сообщений в нужное время. В erlang и elixir есть все для того, чтобы делать такое малой кровью.

Когда приложение запускается, помимо эндпоинта для вебсокетов должны стартовать процессы, которые будут брать первый сниппет в очереди, отправлять его и запоминать новый таймер. Это, в свою очередь, означает, что нужен способ найти процесс и отправить в него сообщение. В elixir есть модуль ровно для этого. В application.ex делаем такое:

Registry это а local, decentralized and scalable key-value process storage. Эта штука позволяет обращаться к процессам не по Pid, а по имени. Так как этот проект не будет запускаться на нескольких нодах, registry — это то что нам нужно. Сам Registy под капотом представляет из себя процесс, который хранит ключ, в нашем случае — id платформы и value: process id erlang-процесса, который занимается отправкой.

Сразу за registry запускаем супервайзер Poster. Из важного там:

При старте этого супервайзера он запускает процессы PosterProc, передавая им параметры для старта.

PosterProc умеет запустится, когда платформа на паузе или нет, а также когда сервис перезапустился спустя какое то время после последней отправки. Для этого я считаю diff, и первая отправка с момента перезапуска «Антихайпа» будет совпадать с тем, что хранится в базе. convert_minutes — это просто функция которая приводит минуты к милисекундам. Самое интересное происходит в schedule_work.

При каждом вызове handle_info :work, args происходит вызов Process.send_after — он, собственно, планирует следующую отправку. Каждый раз, когда это происходит, я запоминаю pid, который возвращает send_after, чтобы иметь возможность найти этот процесс и убить его, если вдруг редактор поставит платформу на паузу или поменял интервал у платформы. В итоге PosterProc всегда хранит в себе следующий стейт:

  • таймер — как часто шлем сообщения (в милисекундах),
  • pid процесса, который в итоге попробует отправить сообщение,
  • id — айди платформы, чтобы по нему найти следующий для отправки сниппет,
  • paused (true или false) — стоит ли этот процесс сейчас на паузе.

Чтобы контролировать процесс снаружи, есть три функции:

Функции pause, unpause и update_timer могут вызываться из процесса сокета, когда редактор меняет статус платформы. Они находят pid PosterProc’а по id из Registry и вызывают соответствующий handle_call.

Когда PosterProc все-таки доходит до момента, когда пора что-то отправить, он вызывает функцию Poster.post(platform_id). В ней происходит поиск первого сниппета в очереди платформы, и он пытается отправится:

Каждый тип платформы — отдельный модуль, при успешной отправке оно отправляет сообщение обратно в сокет.

Фронтэнд.
Мы любим react и redux. Объект, с которым работает фронтэнд, выглядит примерно так

Такая форма представления стейта очень удобна, так-как любой action просто делает deep merge. То есть не важно ,что именно происходит внутри бекэнда — он может в любой момент времени прислать сообщение с частью этого объекта, и эта часть просто вольется внутрь и все перерендерится.

Например, сниппет отправляется. Можно было бы сделать логику на фронтэнде — сделать таймер, который после изменения стейта сниппета со status: pending на status: sending ждет 5 секунд и скрывает. В нашем случае бекенд просто сначала отправляет { snippets: { 1: {status: sending }}} и через пять секунд, асинхронно присылает { snippets: { 1: {status: sent }}} (или что-то другое). Как показала практика, такие вещи куда проще делать на бекенде, чем на фронте.

Для дрег-энд-дропа мы используем react-dnd. При дреге мы хотели менять атрибут только одно сниппета. React-dnd дает большее количество средств для понимания что происходит в какой момент времени и задача свелась к тому, чтобы найти два сниппета между которыми встанет новый, и сделать новый ord который равен (ord2 — ord1) / 2 (По этой причине ord — float). В итоге при любых манипуляциях со сниппетами мы посылаем один update c новым ord.

Постскриптум
Это не первый большой проект на elixir в «Медузе» и совершенно точно не последний. Писать в функциональном стиле — действительно очень-очень классно. Да, безусловно, порог вхождения выше, но оно того стоит. Современный веб он про скорость, синхронизацию и совместное использование и все это, поверьте, куда проще писать функционально.

meduza: how it works

Как работает meduza.io

Meduza

Written by

Meduza

Только самые важные новости и тексты http://vk.com/meduzaproject http://facebook.com/themeduza

meduza: how it works

Как работает meduza.io

Meduza

Written by

Meduza

Только самые важные новости и тексты http://vk.com/meduzaproject http://facebook.com/themeduza

meduza: how it works

Как работает meduza.io

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store