226
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Автоматизируем сервис без API

Время чтения 25 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
226
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Всем привет! Меня зовут Дмитрий Бахтенков, и сегодня я расскажу вам о том, как я упростил себе работу с заметками с помощью Playwright и телеграм-бота. Мы взглянем на self-hosted-заметочник Affine и решим проблему автоматизации с сервисами без публичного API. Приятного чтения!

Автоматизируем сервис без API

Как вы знаете, недавно 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.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники