Joplin это опенсорс приложение для управления заметками, которое я активно использую. Некоторое время назад понадобилось редактировать заметки автоматически. Задача казалась несложной, но по пути удалось собрать достаточно граблей. Пока разбирался, написал микро клиент на go. Заодно поднял в докере и интегрировал в приложение экспериментальный API консольного клиента. Делюсь найденной информацией и кодом.

Какая задача решается

Приложения Joplin используются на разных платформах и поддерживают несколько методов синхронизации данных. В моем случае синхронизируются через S3.

Есть несколько заметок с todo списками (например: работа, домашние дела, списки покупок, хобби). Каждый список в своей заметке. Нужно по определенному признаку выбирать, сортировать и выводить в специальную заметку актуальные задачи. То есть, некоторый автогенерируемый todo лист "на ближайшее время".

Задача в общем виде: периодическое получение данных заметок, обработка и обновление контента выбранной заметки.

Поиск решения

Очевидная идея использовать API. Документация приложения и гугл указали на некий web clipper API. Информации по нему маловато. В CLI приложении режим server с пометкой experimental feature. Первые попытки не увенчались успехом, временно отложил. Ок, что есть еще?

Существует Joplin Server: имплементация сервера синхронизации от авторов приложения. Его API не публичный, сервер не задумывался как API для сторонних клиентов. Технически применить его возможно. Но использовать не публичный API не хочется. Может быть есть еще варианты?

Joplin предоставляет возможность создания собственных клиентов. Для начала попробуем создать свою реализацию клиента с синхронизацией в S3.

Решение 1: ходим напрямую в S3

Для написания клиента придется разобраться с форматами файлов

Файл заметки

Структура файла заметки простая: это текстовый файл .md. Содержит заголовок, тело, метаданные. Разделитель пустая строка:

Заголовок

Контент

id: f23f0132f2124bdea9db91c747853434
parent_id: 55f71434e23b412cb87d3b1cbcff823b
<...>

При редактировании важно не забыть обновить значение параметров updated_time, user_updated_time. Так остальные клиенты подхватят изменения файла при синхронизации.

Файл блокировки

Кроме редактирования заметки, важно не забывать, что есть и другие клиенты, выполняющие в это же время синхронизацию. За консистентность данных отвечает файл блокировки. Он представляет из себя файл с названием формата
locks/<lock_type>_<app_type>_<app_id>.json
Содержимое файла, судя по документации, не имеет значения: в теле те же данные что и в названии. Вероятно тело файла это легаси. На всякий случай будем писать тот же json объект, который пишется в файлы блокировок в других приложениях:

{"type":2,"clientType":1,"clientId":"f5fe97768f3f4188b062a55619b8753e"}

Нас интересует блокировка на запись, exclusive lock в терминах Joplin.

Механизм взятия блокировки из документации

  • Проверяем наличие блокировок

  • Если блокировок нет, создаем файл блокировки и возвращаемся к проверке

  • Если блокировка есть, проверяем владеем ли мы ей. Если мы не владеем блокировкой, возвращаемся к проверке

  • Если блокировка есть и мы им владеем, выполняем изменение

  • Снимаем лок

Для решения текущей задачи этих двух типов файлов хватит. Клиент будет выполнять крон задачу по обновлению заметки

Задача обновления заметки

  • получаем лок на запись

  • процессим:

    • читаем .md файлы

    • фильтруем по parent_id

    • обрабатываем данные

    • собираем контент обновляемой заметки

    • если данные для обновления не изменились, выходим

    • обновляем заметку

  • снимаем лок

Результат можно найти в репозитории

Решение 2: web Clipper API

А если не изобретать велосипед клиент, и не трогать файлы напрямую? У приложения есть API, осталось научиться его готовить. И что вообще такое этот web clipper API?

Web Clipper API это HTTP интерфейс для браузерных расширений. В настройках десктоп приложения можно запустить веб сервер для интеграции с браузером. А кроме GUI у Joplin есть CLI версия. И в CLI приложении можно тоже запустить режим сервера. Итого: запускаем cli, настраиваем синхронизацию, включаем API. Редактируем заметки по API.

Пробуем завернуть в докер. Прокидываем конфигурацию в формате json и стартуем в режиме сервера: joplin server start. Приложение слушает 127.0.0.1:41184. При запуске дополнительно понадобится socat чтобы проксировать на 0.0.0.0. Параметр sync.interval по какой-то причине не запускает синхронизацию. Ок, просто периодически запускаем cli команду joplin sync. API запущено и доступно.

Теперь остается дописать в уже созданное приложение http клиент и реализацию joplin провайдера для http клиента.

Пример реализации и обернутый в докер API в репозитории.
Я реализовал тот же интерфейс, что и для s3. Это не очень оптимально, но в качестве proof of concept сойдет.

Плюсы и минусы решений

Плюсы работы с S3

  • высокая скорость доставки изменений: app -> S3 -> target_client

  • возможности расширения не ограничены

  • формат синхронизации достаточно простой

Минусы работы с S3

  • расширять функциональность сложно, придется разбираться с логикой и форматами

  • используется один конкретный протокол

Плюсы работы с API

  • нет зависимости от формата заметки

  • нет зависимости от протокола синхронизации

  • реализована основная функциональность

Минусы работы с API

  • скорость доставки изменений ниже: схема app -> CLI -> S3 -> target_client

  • сложнее инфраструктура: нужно поднимать дополнительный сервис с API

  • в 2 раза больше дискового пространства (S3+CLI клиент)

  • CLI server экспериментальная опция

  • возможности ограничены методами API

Заключение

Как часто бывает, выбор того или иного решения это компромисс. Я выбрал работать с S3 напрямую. Но также хорошо иметь возможность быстро реализовать нужную функциональность работая по API. Я не стал проверять работу API joplin server, но думаю такая интеграция тоже будет работать.

Спасибо за внимание!

Ссылки

https://joplinapp.org
https://github.com/laurent22/joplin
Как работает lock в joplin
Joplin web clipper

Исходный код