Підписка на новини УАВПП

 
Реєстрація
Забули пароль?

Партнери УАВПП

Посольство США Представництво Програми розвитку ООН в Україні Coca-Cola


27 листопада 2017

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

Facebook Twitter LiveJournal


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

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

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

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

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

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

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

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

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

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

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

Так это работает: https://youtu.be/SOcazH4EtpA

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

Так это выглядит: https://youtu.be/DP5gBVn9YiU

Отдельная важная вещь — публикация видеоматериалов, которых на «Медузе» становится все больше и больше. Раньше видеоотдел публиковал все ролики руками во все социальные сети — это очень трудоемкий процесс (иногда заливка одного видео может занять до 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 выглядит так

Сниппеты упорядочены по 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




Коментарі

Додати коментар