Офтоп Вадим Скворцов
14 461

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

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

{ "author_name": "Вадим Скворцов", "author_type": "editor", "tags": ["\u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b"], "comments": 34, "likes": 47, "favorites": 1, "is_advertisement": false, "subsite_label": "flood", "id": 29656, "is_wide": false, "is_ugc": false, "date": "Fri, 24 Nov 2017 18:32:00 +0300" }
{ "id": 29656, "author_id": 120027, "diff_limit": 1000, "urls": {"diff":"\/comments\/29656\/get","add":"\/comments\/29656\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/29656"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 199791, "possessions": [] }

34 комментария 34 комм.

Популярные

По порядку

Написать комментарий...
15

Самая главная кнопка - в верхнем правом углу.

Ответить
13

Жаль, что название блевотное.

Ответить

6

Фронтенд-разработчик Никита Комарков придумал название для редактора — «Антихайп»

Ничего себе фантазия у Никиты! Какое свежее и оригинальное название, серьёзно. И главное, никем не использовалось ранее

Ответить
2

Самое печальное что разработчик даже не выкупает смысла Антихайпа. Стыд и срам.

Ответить

7

ICQ упадет? А "Агент" останется? :)

Ответить

0

А что надо крикнуть в толпе, чтобы айкью к всех вырос?

Ответить
7

Блокчейн

Ответить
1

ICO

Ответить

5

Господа из Медузы, вы не собираетесь продавать этот продукт? Слэк тоже был разработан как побочный продукт, ребята просто написали чат для своей команды. Если нет, то выложите на гитхаб, вас за это больше любить начнут :)

Ответить
2

Возьмем:
10% vc.ru,
10% tj;
89% Осознание собственной божественной интеллектуальности;
1% вторичности;
Взболтаем в бленднре вж-вж-вж;
———————
= meduza

Ответить
4

найди немного ЕдРа в своих процентах ;)

Ответить
0

Алексей, сущность коммента в том, что медуза скоро начнет писать «карточки» о том как правильно покакать простите (да простит меня Е. Малышева), а не про едро:)

Ответить
3

Не хочу сказать, что все фронтендеры нелюди (да), но автор статьи тоже не проконтроллил что ушло в печать. Сочтём это за чёрную пятницу на Медузе, когда материалы тоже отправляются со значительной скидкой по качеству.

Ответить
3

Какая боль.

Ответить
2

вообще, по описанию выглядит вполне секси

Ответить

1

у вас опечатка — объективного, а не негативного
то, что они совпадают — вина не издания, а наша

Ответить

0

Думаю, что т.к. Антихайп ближе по UI к Trello, он более удобен в использовании для команды Медузы. Ну, и за свой продукт не нужно оплачивать подписку, сколько бы она не стоила )

Ответить

0

Антихайп на века!

Ответить
1

Зомай, успокойся

Ответить
0

А сколько по времени и сколько человек фронтед делали?

Ответить
1

Примерно месяц-полтора, какое то время фронт и бек писал один человек (фронтэндер), так как ему было интересно elixir попробовать. Я ему помогал, потом я включился уже по таймерам, отправке и интеграции с основным приложением.

Ответить
0

А на фронт такого сервиса нужно больше одного человека?

Ответить
0

Незнаю)
Фронт любого банковского приложения не сложнее по функционалу, однако сомневаюсь что у них в штате числится по одному фронтендеру)

Ответить
–5

Придумали они название, прям взяли и сами придумали. Ага.

Ответить

0

ВК Медуза ведёт отвратительно!!! Один и тот же пост может быть 4 РАЗА!

Ответить
0

Логично же. Так у статьи больше охвата будет.

Ответить
0

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

Ответить
0

Ну, всё в разумных пределах )

Ответить
0

А вы не рассматривали вариант общения Антихайпа и Монитора через web-сервис, вместо использования общей БД?

При текущем подходе, если я его правильно понял, чтобы изменить структуру БД без даунтайма надо:
1. Добавить миграцию в Монитор, добавляющую новые поля и таблицы. С обратной совместимостью, чтобы Антихайп после выкладки Монитора продолжал работать.
2. Поправить код Монитора, работающий с БД.
3. Выложить Монитор.
4. Поправить код Антихайпа, работающий с БД.
5. Выложить Антихайп.
6. Добавит миграцию в Монитор, удаляющую старые поля и таблицы.
7. Выложить Монитор.

Получается, что Антихайп слишком сильно завязан на Монитор.
Если же он будет получать данные через сервис, то управлять изменениями будет проще. Плюс, в случае полной недоступности БД Монитора, Антихайп продолжит работать: новые посты нельзя будет запланировать, но уже запланированные продолжат выкладываться.

Ответить
0

Конечно рассматривали. Но нам нужна тесная интеграция. Материал должен знать, что он был / будет отправлен в соц сеть. Мы забираем много мета информации из материала в антихайпе. Например для видео - мы ходим внутрь атрибутов, забираем, например id видео в фейсбуке для кросс постинга. То есть разделять это через апи было бы очень (и излишне) сложно.

По поводу миграций:
мы не удаляем таблицы / колонки. Это не имеет смысла. Они просто перестают использоваться. Если наступит момент, когда захочется привести таблицы в порядок - это произойдет через депрекейт варнинги, и если лог по ним будет пустой - можно будет сносить.
Монитором пользуется ограниченное количество людей, все они есть в слеке. Если произойдет ситуация, когда нужна будет миграция, которая "положит" сервис - мы просто введем мораторий на 10-15 минут - все сделаем и разрешим пользоваться дальше.

"Плюс, в случае полной недоступности БД Монитора" - в этой ситуации, нам не будет дела до антихайпа. Мы все с горящими попами будем чинить эту проблему.

В итоге то что антихайп сильно связан с монитором - для нас это плюс.

Ответить
0

"Фронтенд-разработчик Никита Комарков придумал название для редактора — «Антихайп». "
Ага, молодец какой. Оригинальный главное.

Ответить
0

Не всем дано понять юмор нашего фронтенд-разработчика. Не расстраивайтесь.

Ответить
0

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

Ответить
0

А где можно доступ приобрести? Я бы взял.

Ответить

0
{ "page_type": "article" }

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "bscsh", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-223676-0", "render_to": "inpage_VI-223676-0-1104503429", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=bugf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudx", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byzqf", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvc" } } }, { "id": 19, "label": "Тизер на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "p1": "cbltd", "p2": "gazs" } } } ]
Команда калифорнийского проекта
оказалась нейронной сетью
Подписаться на push-уведомления
{ "page_type": "default" }