Как вы знаете, недавно Notion ушел из России, а это был очень удобный сервис для заметок, которым я долго пользовался. Именно поэтому, узнав об уходе, я задумался о том, что пора бы уже хранить свои заметки где-то у себя.
И вот от одного из своих коллег я узнал про Affine — self-hosted open source заметочник, который позиционирует себя как аналог Notion и Miro в одном приложении. Я его у себя развернул, попробовал — и мне всё понравилось. Всё, кроме одного момента…
Мобильная версия сайта
Её фактически нет. Открытая заметка на телефоне выглядит так:
Конечно, можно использовать функционал «Открыть версию для ПК» в браузере, но это тоже не очень удобно: например, есть проблемы с масштабом и полями для ввода.
А что мне нужно от мобильного приложения? Всего лишь Inbox, который я буду записывать в ежедневную заметку в течение дня, а потом разбирать все эти записи.
Можно ли это сделать как-то удобнее?
Когда я задумываюсь об удобстве, мне в голову всегда приходят мысли про телеграм-ботов. Телеграм — это мой основной мессенджер, куда было бы удобно отсылать всякие мелочи для разбора. (Я даже делал свой таск-трекер внутри мессенджера.) Saved Messages мне не очень нравятся, но вот отправлять заметки через бота в Affine по API, на мой взгляд, звучит очень круто!
Но вот незадача…
У Affine нет Open API! Что, наверное, логично: создать API под такое приложение может быть сложно, да и в open source не все имеют столько времени.
И тут я вспомнил: я давно хотел изучить работу с браузерами через какой-нибудь Selenium или Playwright. Кажется, что данная ситуация — отличный повод это сделать.
Playwright
Playwright — это фреймворк для написания e2e-тестов веб-приложений. По сути, это движок, который открывает веб-страницу в браузере и делает с ней «всякое»: жмет на кнопки, вводит данные в формы и так далее. Затем в рамках теста мы проверяем, что всё хорошо: данные из формы сохранились, отображаются корректно, ошибок нет.
Я не собирался писать тесты, но собирался взаимодействовать со страницей в браузере: открыть ее, залогиниться, перейти в журнал и добавить запись с текстом, который отправил боту в Телеграм. И этот инструмент полностью решает мою задачу.
Устанавливаем Playwright
Несмотря на то что я дотнетчик, я решил добавить разнообразия в свою жизнь и использовать Python. Этот язык я тоже знаю и люблю писать на нём маленькие штуки для маленьких автоматизаций.
Для установки Playwright я создал виртуальное окружение, в котором выполнил команды:
pip install playwright
playwright install
После дождался установки пакетов — это может быть немного долго.
Первый заход
Сначала попробуем использовать Playwright без телеграм-бота, чтобы понять, как вообще им пользоваться.
Всё начинается с методов sync_playwright или async_playwright, которые возвращают объект Playwright для взаимодействия с браузером.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
#работаем с браузером
Далее мы можем открыть браузер, например Chromium или Firefox. Это управляется с помощью свойства объекта Playwright, а запускаемся мы с помощью метода launch. Параметр headless указывает, запускать ли окно браузера или нет. Для отладки поставим False, чтобы посмотреть на действия в браузере.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://affine.pro/')
При запуске этого кода у вас откроется браузер и произойдет переход по ссылке: https://affine.pro.
Теперь в параметрах метода goto укажем адрес поднятой Affine с необходимым нам пространством и попробуем перейти по этому адресу. Мы попадем на страницу входа:
Чтобы войти, нам необходимо ввести свой email и нажать на Continue with email. Давайте посмотрим на эти элементы в инструментах разработчика, чтобы понять, как нам найти эти элементы в браузере:
- Для поля ввода почты можно воспользоваться атрибутом placeholder.
- Для кнопки у нас есть атрибут data-testid, который используется для идентификации элементов в автотестах.
Примечание: разработчики Affine сами пишут тесты с помощью Playwright, поэтому на многих элементах веб-страницы уже установлены удобные data-testid. Спасибо им за это!
Давайте попробуем заполнить наш email и кликнуть по кнопке. Для этого добавим следующий код:
page.fill('input[placeholder="Enter your email address"]', 'dmbahoff@ya.ru')
page.click('[data-testid="continue-login-button"]')
Строка input[placeholder=”Enter your email address”] — это селектор, который позволяет как-то идентифицировать HTML-элемент для взаимодействия. Сначала указываем наименование тега, а затем выражение с каким-то атрибутом и его значением. Таким образом мы будем находить элементы и делать с ними «всякое».
А пока запустим наш код:
Отлично! Теперь форма требует ввода пароля. Посмотрим в dev tools, как нам найти поле с паролем:
И вот у нас уже есть два testid — для пароля и для кнопки входа. Можем их использовать:
page.fill('input[data-testid="password-input"]', 'тут пароль')
page.click('button[data-testid="sign-in-button"]')
И вот мы успешно авторизовались:
Теперь мы видим один из больших недостатков Affine — она периодически выплевывает модалку с предложением включить AI. Давайте посмотрим, как ее закрыть:
page.click('button[data-testid="modal-close-button"]')
У нас получилось авторизоваться и закрыть рекламную модалку. Таким образом мы и будем взаимодействовать с Affine. Этим и прекрасен Playwright: его API удобный и понятный, и его можно использовать даже без глубоких знаний по теме.
Полный код взаимодействия с Affine
Подобным образом я нашел все необходимые элементы и в конечном счете заполнил текстовое поле для строки в заметке:
def create_affine_note(text: str):
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('адрес воркспейса в Affine')
page.fill('input[placeholder="Enter your email address"]', 'ваш email')
page.click('[data-testid="continue-login-button"]')
page.fill('input[data-testid="password-input"]', 'ваш пароль')
page.click('button[data-testid="sign-in-button"]')
page.click('button[data-testid="modal-close-button"]')
page.click('div[data-testid="slider-bar-journals-button"]')
page.click('div[data-testid="page-editor-blank"]')
locator = page.locator('div[data-testid="page-editor-blank"]')
locator.focus()
locator.press_sequentially(text)
locator.press('Enter')
browser.close()
Телеграм-бот
Написать телеграм-бота на Python с одной командой добавления заметки — это довольно тривиальная задача. На просторах интернета есть очень много статей на тему «hello world в телеграм-боте».
Своего бота можно создать через телеграм-бота @BotFather. Нам понадобится только токен, который мы укажем в конфигурации.
Мы воспользуемся библиотекой pyTelegramBotAPI:
pip install pyTelegramBotAPI
Определим требования к телеграм-боту:
- Какая-то аутентификация: мы не хотим, чтобы Affine заддосили люди, случайно нашедшие этого бота. Будем проверять свой userId из Телеграма.
- При отправке сообщения с каким-то текстом заходим в ежедневную заметку Affine и добавляем к ней указанный текст.
Конфигурация
Добавим в наш проект файл ENV и заполним его следующим образом:
AFFINE_WORKSPACE_URL=''
AFFINE_USER_EMAIL=''
AFFINE_USER_PASSWORD=''
IS_DEBUG='True'
TELEGRAM_API_KEY=''
USER_ID=''
Эти параметры позволят нам более гибко управлять нашим ботом. А чтобы загрузить данные из ENV-файла в приложение, воспользуемся библиотекой python-dotenv:
pip install python-dotenv
Теперь получим параметры из переменных окружения — добавим файл confg.py со следующим содержимым:
import os
TELEGRAM_API_KEY = os.getenv('TELEGRAM_API_KEY')
AFFINE_WORKSPACE_URL = os.getenv('AFFINE_WORKSPACE_URL')
AFFINE_USER_EMAIL = os.getenv('AFFINE_USER_EMAIL')
AFFINE_USER_PASSWORD = os.getenv('AFFINE_USER_PASSWORD')
IS_DEBUG = os.getenv('IS_DEBUG') == 'True'
USER_ID = os.getenv('USER_ID')
Приводим в порядок Affine
Теперь добавим файл affine.py (или поправим существующий), чтобы использовать конфиг вместо «прибитых гвоздями» логина, пароля и адреса пространства:
from playwright.sync_api import sync_playwright
import config
def create_affine_note(text: str):
with sync_playwright() as p:
browser = p.chromium.launch(headless=config.IS_DEBUG is False)
page = browser.new_page()
page.goto(config.AFFINE_WORKSPACE_URL)
page.fill('input[placeholder="Enter your email address"]', config.AFFINE_USER_EMAIL)
page.click('[data-testid="continue-login-button"]')
page.fill('input[data-testid="password-input"]', config.AFFINE_USER_PASSWORD)
page.click('button[data-testid="sign-in-button"]')
page.click('button[data-testid="modal-close-button"]')
page.click('div[data-testid="slider-bar-journals-button"]')
page.click('div[data-testid="page-editor-blank"]')
locator = page.locator('div[data-testid="page-editor-blank"]')
locator.focus()
locator.press_sequentially(text)
locator.press('Enter')
browser.close()
Основная логика бота
И, наконец, добавим файл main.py с основной логикой приложения:
import telebot
import config
from telebot import types
from affine import create_affine_note
import dotenv
#загрузим ENV-файл
dotenv.load_dotenv()
# создадим объект бота для взаимодействия с Telegram API
bot = telebot.TeleBot(config.TELEGRAM_API_KEY)
# обработчик команды start
@bot.message_handler(commands=['start'])
def start_bot(message: types.Message):
if str(message.from_user.id) != config.USER_ID:
bot.send_message(message.chat.id, 'Некорректный пользователь!')
return
first_mess = f"Привет! Я бот для создания заметок в Affine."
bot.send_message(message.chat.id, first_mess)
# обработчик остальных команд
@bot.message_handler()
def create_note(message: types.Message):
if str(message.from_user.id) != config.USER_ID:
bot.send_message(message.chat.id, 'Некорректный пользователь!')
return
create_affine_note(message.text)
bot.send_message(message.chat.id, 'Заметка успешно создана!')
if __name__ == '__main__':
# включаем поллинг, чтобы получать обновления от Telegram API
bot.infinity_polling()
Как узнать свой userId? Это можно сделать двумя способами:
- Во время отладки, когда придет первое сообщение от бота.
- Через других специальных телеграм-ботов, например https://t.me/username_to_id_bot (не знаю, насколько это безопасно).
Запускаемся
Заполняем файл ENV и запускаем файл main.py. Теперь наша программа «слушает» обновления от бота.
Переходим в чат с ботом и нажимаем на Start:
Получаем ID пользователя, если вы не сделали это ранее, и правим конфигурацию.
Теперь попробуем написать какой-то текст боту.
Перейдём в Affine и проверим:
Деплой бота на сервер
Я люблю использовать Docker для деплоя всяких штук на выделенный сервер, который можно арендовать у множества провайдеров. Этот бот фактически является сервисом — просто приложение, которое висит на фоне и слушает обновления от Телеграма.
Подготовим файл requirements.txt, а затем dockerfile.
requirements.txt:
- Можно написать туда зависимости самостоятельно.
- А можно, используя venv, выполнить команду pip3 freeze > requirements.txt.
certifi==2024.8.30
charset-normalizer==3.4.0
greenlet==3.1.1
idna==3.10
playwright==1.48.0
pyee==12.0.0
pyTelegramBotAPI==4.23.0
python-dotenv==1.0.1
requests==2.32.3
typing_extensions==4.12.2
urllib3==2.2.3
Dockerfile:
FROM mcr.microsoft.com/playwright/python:v1.48.0-jammy
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["/bin/bash", "-c", "python main.py"]
В этом dockerfile мы берем базовый образ Playwright, устанавливаем зависимости и запускаем файл main.py.
Для запуска бота в контейнере заполните файл ENV необходимыми параметрами и выполните команды:
docker
build . -t affine_bot:lts
docker
run –env-file ./.env affine_bot:lts
Поздравляю! У нас готов бот для добавления заметок в Affine! Теперь можно залить образ в docker registry и использовать его на своем сервере или просто склонировать репозиторий и собрать docker-образ.
Что можно улучшить?
Во-первых, не всем удобно писать заметки именно в журнал, в заметку дня. У кого-то это может быть статичная заметка с названием Inbox или еще что-то. Можно добавить конфигурацию того, куда добавлять записи.
Во-вторых, можно реализовать чтение заметок с возможностью выбрать воркспейс, папку и так далее. Это чуть сложнее, но неплохо расширит функционал бота.
Заключение
Отсутствие API — это не повод расстраиваться и не автоматизировать работу с разными сервисами. Playwright может быть отличным инструментом не только для тестов, но и для таких «домашних» автоматизаций. Пользуйтесь им на здоровье!
Кстати, я веду телеграм-канал, где пишу про разработку. А вот тут — репозиторий с проектом на GitHub.