Вступление

Первые дни Клабхауса в России были чудесными - люди, с которыми большинство из нас вряд ли когда-то сможет побеседовать вживую, рассказывают дико интересные вещи, бесплатно и без СМС. Я написал бота, который, по субъективным оценкам, в пике записывал топ250 всех мировых стримов и использовался пятизначным количеством пользователей. Я расскажу, как сделать аналогичное для собственных нужд.

Дисклеймер
Возможно, что все описанное ниже нарушает ToS Клабхауса. Используйте на свой страх и риск и только в образовательных целях.

Все началось с Тинькова и Яндексовых комнат. Атмосфера пятничных Хуралов, интересные истории, и всего несколько проблем:

  • во-первых, я уже давно привык слушать подкасты (а Клабхаус я воспринимал исключительно как read-only) на х2, чего нельзя сделать по определению в лайве.

  • во-вторых, все как будто сговаривались на начинали вещать примерно в одно и то же время

  • в-третьих, иногда чаще всего они делали это в совершенно непотребное время, вроде разгара рабочего дня или утра в воскресенье.

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

Глава 1. Рассвет

Итак, у нас есть задача, которую нужно решить. Выбор сразу пал на телеграм - боты дают широчайший простор для творчества, позволяют сэкономить кучу времени за счет отсутствия необходимости пилить какую-нибудь вебморду и так далее. Задача усугублялась тем, что вообще-то я не умею программировать. Еще сильнее она усугублялась тем, что я, в силу некоторой стрёмности задачи, хотел явным образом отделить себя-исследователя от себя-наемного_работника, поэтому решал задачу в максимально нерабочее время, без использования корпоративного оборудования, вычислительных ресурсов и чего-то еще подобного.

Началось все с поиска хотя бы APIшечки. Ныне широко известный в узких кругах репозиторий имел весьма базовый вид, поэтому принципы работы нужных ручек приходилось периодически угадывать. Далее пришлось разобраться с Agora, которая, как известно, собственно и обеспечивает всю голосовую часть Клабхауса. Я всерьёз опасался, что мне придется реализовывать какой-нибудь сверхупрощенный клиент, но оказалось, что у них есть чудесный SDK, с примерами кода. Это не сказать чтобы прям решило мою задачу (я все еще менеджер и не умею программировать), но значительно продвинуло меня в понимании сути происходящего. Примерно с 2 до 4 ночи я занимался исключительно познавательной задачей по допиливанию существующего в примерах плюсового кода для решения моей конкретной задачи. Когда свежескомпилированный бинарник успешно заработал, генеря аудиофайлик с нужными метаданными, я удивился и обрадовался одновременно.

За окном зарождался новый день, а я начал набрасывать клиентскую часть. Быстро вырисовалась базовая схема. Получаем от пользователя ссылку на активную комнату или на событие в будущем. Если это активная комната, то дергаем ручку /join_channel, получаем оттуда токен и название комнаты. Сразу же выходим из комнаты, и передаем полученные данные специальному демону, который делает простейшую вещь: если это новая комната, то он стартует отдельный бинарник, который, собственно, делает непосредственно всю работу по записи аудиопотока и записывает chat_id телеграммной беседы, чтобы обрадовать счастливого слушателя, когда всё будет готово. Если же это уже известная комната, которая уже записывается, то, разумеется, новый поток не запускается, но слушатель дописывается в список тех, кто решил записать эту беседу раньше.

Если же это событие в будущем (event в терминах Clubhouse), то начинается веселье. С некоторой периодичностью, которая увеличивается со временем, мы начинаем дергать ручку /get_event. Мы извлекаем оттуда ориентировочное время старта беседы (потому что оно может меняться со временем) и, если беседа уже началась, идентификатор комнаты. Как только это происходит, бот информирует всех заинтересованных о том, что беседа началась и далее автоматически происходит процедура из предыдущего абзаца. Бывают случаи, когда событие так и не начинается. В этом случае бот убивает событие в очереди демона и отправляет грустное сообщение интересантам.

Параллельно, в фоне, работает отдельный процесс, смысл которого сводится буквально к следующему. Как только в папках с записями появляется файлик-флажок, свидетельствующий о том, что беседа закончена, запускается ffmpeg, который разбивает запись на часовые кусочки. Далее все они по очереди отправляются каждому из заинтересованных слушателей. После происходит удаление всей накопленной информации - самих записей, данных из БД, логов и так далее.

Глава 2. Расцвет

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

Храбрая впска записывает под сотню стримов одновременно

Глава 3. Закат

Первый бан - RPS ручек

В какой-то момент я заметил, что график ошибок стал расти как-то совершенно неприлично. Оказалось, что Clubhouse ввел ограничения по частоте дергания каждой ручки. Пришлось быстро переписать логику. Теперь все задания не отправлялись в немедленное исполнение, а складывались в отдельную очередь, куда периодически ходила отдельная функция и пыталась сгенерировать токены. Веселье продолжилось, количество потоков записи стало уверенно трехзначным.

Второй бан - Agora

WARN (10:24:22:783 | 0) 34352; [ch0] receive notification 14 from server
INFO (10:24:22:783 | 0) 34352; [ch0] connection rejected due to client is banned, code=14
INFO (10:24:22:784 | 1) 34352; [vos] [rejected] client is banned by vos 104.166.161.21:4016

Ребята, очевидно, подзадобались платить за чужих людей и начали банить на уровне агоры. Насколько я могу судить, это бан именно на уровне учетки, не IP. Борьба может быть продолжена путем генерации новых учеток, но это уже не так интересно.

Глава 4. Новый ренессанс или Запись Clubhouse - сделай сам

1) Заведи себе линуксовый сервер. Если речь идет про личное использование, то подойдет вообще любой, даже самый дешевый. Установи туда ffmpeg.

2) Скачай SDK и распакуй полученный архив.

3) Скомпилируй рекордер. Если совсем лень заморачиваться, сделай все буквально, как написано здесь.

4) Получи авторизационный токен и личный идентификатор пользователя. Если лень разбираться - скачай питоновую либу, запусти скрипт cli.py и авторизуйся. Интересующие тебя данные будут находиться в settings.ini. Если же ты не ищешь простых путей, то тебя интересует последовательное выполнение client.start_phone_number_auth('+7123467890') и далее, после получение смски, client.complete_phone_number_auth('+7123467890', '1234'). В результате выполнения этой функции вернется словарь, авторизационный токен находится в result['auth_token'], а личный идентификатор - в result['user_profile']['user_id']. В возвращаемом заголовке CH-DeviceId находится идентификатор устройства. Запиши их куда-нибудь, они константны. При вызове всех методов ты будешь передавать их примерно вот так:

client = Clubhouse(    
    user_id='личный идентификатор пользователя',    
    user_token='авторизационный токен',
    user_device='идентификатор устройства'
)


5) Получи токен для записи выбранной комнаты. Для этого используй метод client.join_channel(room_id), где room_id это последняя часть ссылки https://joinclubhouse.com/room/ROOMID. Если комната уже активна, в ответе прилетит поле token. Не забудь выйти из канала, client.leave_channel(room_id)


После этого запускаем рекордер, примерно так:

./recorder_local --channel ROOMID --appId 938de3e8055e42b281bb8c6f69c21f78 --uid личный_идентификатор_пользователя --channelKey токен_для_канала --appliteDir bin --isMixingEnabled 1 --isAudioOnly 1 --idle 60 --recordFileRootDir records --logLevel 2

Запись будет окончена, когда в свежесозданной папке появится файлик recording2-done.txt.

6) Запись запланированных мероприятий чуточку сложнее. Сначала нужно подергать метод client.get_event(event_hashid=event_id), где event_id это последняя часть ссылки https://www.joinclubhouse.com/event/EVENTID. Если событие уже началось, то этот метод вернет непустое поле result['event']['channel']. В нем содержится идентификатор комнаты, с которым далее следует сделать все, описанное в предыдущем пункте. Если же это поле пустое, то мероприятие еще не началось.