Аналог Trello для работы с соцсетями

Как издание «Медуза» разработало свой социальный редактор «Антихайп».

В избранное

В избранном

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

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

Год назад мы поняли, что нас не устраивает ситуация с социальными сетями. Это очень больно — писать текст «подводок» в трёх разных интерфейсах, в Facebook, «ВКонтакте» и Twitter, делать отложенные публикации, следить за тем, чтобы записи не «каннибализировали» друг друга. Например, в случае с Facebook и его «умной» лентой охват одного сообщения может сильно упасть, если другое было опубликовано в то же время.

Ситуация становится хуже, когда наступает кризис, например, если где-то произошёл теракт. Приходится бежать в социальные сети и убирать старые отложенные записи, публиковать более срочные про актуальное событие. Загрузка видео в соцсети — отдельная боль, тут к неудобству интерфейса добавляется долгое время загрузки, ведь загрузить видео нужно в каждой из трёх соцсетей в отдельности, а для публикации на сайте ещё и в YouTube.

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

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

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

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

Много новостей — 30 минут в Facebook, 10 минут в Twitter. Выходные — по часу везде. Каждая платформа может быть независимо от другой очищена, если произошло очень важное событие, и вся редакция работает по одной теме. Каждая платформа может встать на паузу — удобно, когда в спокойном режиме хочется собрать публикаций на ночь.

Каждая карточка или сниппет — это статус и ссылка на материал. Также динамическое время публикации, которое рассчитывается в зависимости от таймера платформы, даты последней публикации и количества сниппетов, стоящих перед этой карточкой.

Когда редактор собирает сниппеты для платформы, он тянет их из результатов поиска, пишет или редактирует статус и ставит в очередь. Когда наступает время, сниппет отправляется в социальную сеть.

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

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

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

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

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

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

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

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

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

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

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

Аналитика

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

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

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

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

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

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

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

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

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

У нас есть приложение, в котором редакторы пишут материалы, формируют главную страницу и вообще работают. Это приложение называется «Монитор», тут можно почитать про него подробнее. Этому проекту уже три года, он написан на Ruby on Rails. Когда я думал о том, как писать «Антихайп», то понял, что у меня нет никого желания писать его на Ruby.

Поймите меня правильно: Ruby on Rails — отличная штука, но такие вещи, как параллельная работа, отложенные вычисления и что, наверное, самое важное, вебсокеты — не самые ее сильные стороны. Да, я в курсе про action cable, но что-то не хочется. И так как мы любим микросервисы, я решил, что пусть это будет elixir и phoenix framework. Я решил, что:

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

Модели

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

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

Так — Snippet:

Сниппеты упорядочены по 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 ждет пять секунд и скрывает. В нашем случае бекэнд просто сначала отправляет { snippets: { 1: {status: sending }}} и через пять секунд асинхронно присылает { snippets: { 1: {status: sent }}} или что-то другое. Как показала практика, такие вещи куда проще делать на бекэнде, чем на фронте.

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

Постскриптум

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

#инструменты

©  vc.ru