diff --git a/.gitignore b/.gitignore index 7332770..88410ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -config.ini +config.toml post_id.json diff --git a/Dockerfile b/Dockerfile index bcb693b..3939a96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ -FROM alpine:latest +FROM alpine:3.16.2 +COPY . /opt/vk-toot +WORKDIR /opt/vk-toot RUN apk update && apk add python3 py3-pip -RUN mkdir -p /opt/vk-mastodon-bridge -ADD . /opt/vk-mastodon-bridge -WORKDIR /opt/vk-mastodon-bridge RUN pip install --upgrade pip && pip install --requirement requirements.txt -CMD python3 vk_mastodon_bridge.py +CMD python3 vk_toot.py diff --git a/README.md b/README.md index 1e88d70..e00094a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,42 @@ -# vk-mastodon-bridge +# VK-Toot -Бридж для кросспостинга из [VK](https://vk.com) в [Mastodon](https://joinmastodon.org/). Это приложение будет дублировать посты из паблика или публичной группы VK в аккаунт Mastodon. +Кросспостинг из [VK](https://vk.com) в [Mastodon](https://joinmastodon.org/). +Это приложение будет дублировать посты из паблика или публичной группы VK в +аккаунт Mastodon. Пример аккаунта бота: [@jrokku@mas.to](https://mas.to/@jrokku) ## Как это работает -Скрипт согласно заданному промежутку времени (см. ниже `POLLING_TIME`) ходит в API VK и забирает оттуда самый свежий пост, запоминает его ID в файл. Затем ходит в API Mastodon и создаёт новый пост, который содержит оригинальный текст поста, ссылку на оригинальный пост, ссылки на каждое вложение из оригинального поста. +Скрипт согласно заданному промежутку времени (см. ниже `POLLING_TIME`) ходит в +API VK и забирает оттуда самый свежий пост, запоминает его ID в файл. Затем +ходит в API Mastodon и создаёт новый пост, который содержит оригинальный текст +поста, ссылку на оригинальный пост, ссылки на каждое вложение из оригинального +поста. ## Известные проблемы/TODO -- Не учитывается длина поста. Если исходный пост не будет укладываться в лимит символов на инстансе Mastodon'а, то неизвестно что произойдёт. Решение: надо обрезать текст поста в функции `build_post()`. -- Никак не обрабатываются вложения типов отличных от фото (`photo`) и фотоальбома (`album`). Надо добавить хотя бы `video`. -- Указывать в списке вложений в посте только те вложения, которые не были загружены в Mastodon. Пока формируется полный список вложений. +- Не учитывается длина поста. Если исходный пост не будет укладываться в + лимит символов на инстансе Mastodon'а, то пост не опубликуется. Решение: + надо обрезать текст поста в функции `build_post()`. Логика сложнее, чем + кажется, поэтому я жду озарения. +- Никак не обрабатываются вложения типов отличных от фото (`photo`) и + фотоальбома (`album`). Надо добавить хотя бы `video`. +- Указывать в списке вложений в посте только те вложения, которые не были + загружены в Mastodon. Пока формируется полный список вложений. - Добавить поддержку `group_id`. +- Добавить флаг `SHOW_ATTACHMENTS_LIST` для возможности скрытия списка + вложений из текста. +- Добавить `mastodon.POST_CHAR_LIMIT` для обрезки текста относительно лимита + инстанса. ## Настройки и запуск ### API Mastodon -Для получения токена API Mastodon достаточно добавить приложение в своём профиле (Preferences -> Development). Из разрешений достаточно `write` для создания постов, ничего другого бридж делать не умеет. +Для получения токена API Mastodon достаточно добавить приложение в своём +профиле (Preferences -> Development). Из разрешений достаточно `write` для +создания постов, ничего другого бридж делать не умеет. ### API VK @@ -29,64 +46,85 @@ https://oauth.vk.com/authorize?client_id=12345678&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=offline&response_type=token&v=5.131 -Здесь параметр `client_id` равняется `APP_ID` приложения, `scope=offline` задаёт нулевой (бесконечный) срок жизни токена. +Здесь параметр `client_id` равняется `APP_ID` приложения, `scope=offline` +задаёт нулевой (бесконечный) срок жизни токена. -При переходе по ссылке произойдёт редирект на страницу с подтверждением выдачи доступа приложению и далее редирект на страницу с предупреждением. Нас интересует значение query-параметра `access_token` в адресной строке. +При переходе по ссылке произойдёт редирект на страницу с подтверждением +выдачи доступа приложению и далее редирект на страницу с предупреждением. +Нас интересует значение query-параметра `access_token` в адресной строке. Это всё также описано в [документации к API](https://dev.vk.com/api/access-token/implicit-flow-user). ### Конфигуарация -Конфигурация задаётся в файле `./data/config.ini` и разделена на три секции: `mastodon`, `vk` и `bridge`. Последний содержит параметры приложения, первые два реквизиты API. См. `./data/config.example.ini`. +Конфигурация задаётся в файле `./data/config.toml` и разделена на три секции: +`mastodon`, `vk` и `bridge`. Последний содержит параметры приложения, первые +два реквизиты API. См. `./data/config.example.toml`. -| Секция | Переменная | Умолчание | Пример | Описание | -| --------- | --------------------- | --------- |---------------------------------- | ---------------------------------------------- | -| mastodon | `API_URL` | нет | https://mastodon.social/api/v1 | URL адрес API Mastodon. | -| mastodon | `API_ACCESS_TOKEN` | нет | | Ключ API Mastodon. | -| vk | `API_URL` | нет | https://api.vk.com/method | URL API VK. | -| vk | `API_VERSION` | нет | 5.131 | Версия VK API. | -| vk | `API_ACCESS_TOKEN` | нет | | Ключ API VK. | -| vk | `GROUP_DOMAIN` | нет | apiclub | slug адрес группы/паблика VK. | -| bridge | `POLLING_TIME` | 300 | 300 | Задержка получением постов из VK в секундах. | -| bridge | `REQUEST_DELAY` | 1 | 1 | Задержка загрузки медиа в Mastodon в секундах. | +#### Mastodon (`mastodon`) + +* `API_URL`: URL адрес API Mastodon. Пример: **https://mastodon.social/api/v1** +* `API_ACCESS_TOKEN`: Ключ API Mastodon. + +#### VK (`vk`) + +* `API_URL`: **https://api.vk.com/method** URL API VK. +* `API_VERSION`: **5.131** Версия VK API. +* `API_ACCESS_TOKEN`: Ключ API VK. +* `GROUP_DOMAIN`: slug адрес группы/паблика VK. Пример: **apiclub**. + +#### Настройки VK-Toot (`bridge`) + +* `POLLING_TIME`: **300** Задержка получения постов из VK в секундах. +* `REQUEST_DELAY`: **1** Задержка загрузки медиа в Mastodon в секундах. Если + Возникают ошибки 429 Too Many Requests, то следует увеличить эту цифру. ### Запуск без Docker -Переименуйте `config.example.ini` в `config.ini` и отредактируйте значения в нём. +Переименуйте `config.example.toml` в `config.toml` и отредактируйте его. ``` pip install -r requirements.txt -python vk-mastodon-bridge.py +python vk_toot.py ``` ### Docker +Переименуйте `config.example.toml` в `config.toml` и отредактируйте его. + Сборка образа: ``` -sudo docker build -t vk-mastodon-bridge:0.2.1 . +sudo docker build -t vk-toot:0.3.0 . ``` Запуск контейнера: ``` -sudo docker run --detach \ - --name vk-mastodon-bridge \ - --volume /opt/vk-mastodon-bridge/data:/opt/vk-mastodon-bridge/data \ - vk-mastodon-bridge:0.2.1 +sudo docker run \ + --detach \ + --name vk-toot \ + --volume /opt/vk-toot/data:/opt/vk-toot/data \ + vk-toot:0.3.0 ``` ## История изменений -### Next +### 0.3.0 -- Переменные окружения заменены на config.ini. +- BREAKING! Приложение переименовано в vk-toot. +- BREAKING! Вместо переменных окружения теперь используется config.toml. Для + изменения параметров больше не нужна пересборка контейнера. +- Улучшено логирование. ### 0.2.1 -- Исправлена публиация постов. Mastodon API мог отвечать кодом 429 из-за слишком частых загрузок медиа. Решено через добавление задержки `REQUEST_DELAY`. +- Исправлена публиация постов. Mastodon API мог отвечать кодом 429 из-за + слишком частых загрузок медиа. Решено через добавление задержки + `REQUEST_DELAY`. - Исправлена ошибка при посте статуса с менее чем 4-я вложениями. -- Файл `./data/last_post_id` теперь содержит JSON и называется `./data/post_id.json`. ID хранится в единственном поле `post_id`. +- Файл `./data/last_post_id` теперь содержит JSON и называется + `./data/post_id.json`. ID хранится в единственном поле `post_id`. - Имя модуля теперь не содержит дефисов. - Добавлено логирование. Лог пишется в STDOUT. @@ -94,7 +132,8 @@ sudo docker run --detach \ - Реализована загрузка вложений в Mastodon (до 4-х штук). Только изображения. - Обновлён путь до приложения в Dockerfile. -- Изменено место хранения файла `last_post_id`, теперь он в директории `data/`, откуда его можно удобно монтировать как Docker volume. +- Изменено место хранения файла `last_post_id`, теперь он в директории + `data/`, откуда его можно удобно монтировать как Docker volume. ### 0.1.0 diff --git a/data/config.example.ini b/data/config.example.toml similarity index 100% rename from data/config.example.ini rename to data/config.example.toml diff --git a/requirements.txt b/requirements.txt index d15ce5a..87ed10a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests==2.28.1 +toml==0.10.2 diff --git a/vk_mastodon_bridge.py b/vk_toot.py similarity index 83% rename from vk_mastodon_bridge.py rename to vk_toot.py index 932aecb..b007778 100644 --- a/vk_mastodon_bridge.py +++ b/vk_toot.py @@ -8,13 +8,13 @@ import datetime import logging import shutil import urllib.parse -import configparser import requests +import toml -config = configparser.ConfigParser() -config.read('./data/config.ini') +with open('./data/config.toml', 'r') as file: + config = toml.loads(file.read()) MASTODON_API_URL = config['mastodon']['API_URL'] MASTODON_API_ACCESS_TOKEN = config['mastodon']['API_ACCESS_TOKEN'] @@ -24,11 +24,11 @@ VK_API_ACCESS_TOKEN = config['vk']['API_ACCESS_TOKEN'] VK_GROUP_DOMAIN = config['vk']['GROUP_DOMAIN'] # Set up logger -logger = logging.getLogger('vk_mastodon_bridge') -logger.setLevel(logging.INFO) +log = logging.getLogger('vk_toot') +log.setLevel(logging.INFO) handler = logging.StreamHandler(stream=sys.stdout) handler.setFormatter(logging.Formatter(fmt = '[%(asctime)s: %(levelname)s] %(message)s')) -logger.addHandler(handler) +log.addHandler(handler) def get_vk_group_last_post(): """Return dict with VK group last post data.""" @@ -107,7 +107,7 @@ def post_media(file: str) -> str: headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN} files = {'file': open(file,'rb')} response = requests.post(MASTODON_API_URL + '/media', files=files, headers=headers) - logger.info('Post media on Mastodon. Response: ' \ + log.info('Post media on Mastodon. Response: ' \ + str(response.status_code) + ' ' + str(response.text)) # Sleep some seconds to prevent HTTP 429 response. try: @@ -124,12 +124,13 @@ def publish_toot(post_text: str, media_ids: list): headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN} params = {'status': post_text, 'media_ids[]': media_ids} response = requests.post(MASTODON_API_URL + '/statuses', data=params, headers=headers) + log.info('Publish toot. Response: %s' % response.text) post_url = json.loads(response.text)['url'] - logger.info('Publish status. Response: ' \ - + str(response.status_code) + ' Status: ' + str(post_url)) + log.info('Status: ' \ + + str(response.status_code) + ' URL: ' + str(post_url)) return response -def post_toot(post_data: dict) -> str: +def toot(post_data: dict) -> str: """Upload media files, generate status text for Mastodon post and publish. """ post_text = get_vk_post_text(post_data) @@ -140,7 +141,7 @@ def post_toot(post_data: dict) -> str: # Upload only first 4 photos and get media_ids i = 0 media_ids = [] - logger.info('Attachments: %s' % str(attachments)) + log.info('Attachments: %s' % str(attachments)) attachments_count = len(attachments) if attachments_count > 4: @@ -149,12 +150,12 @@ def post_toot(post_data: dict) -> str: while i < attachments_count: if list(attachments[i].keys())[0] == 'photo': photo_url = attachments[i]['photo'] - logger.info('Download image: %s' % photo_url) + log.info('Download image: %s' % photo_url) tmpfile = download_file(photo_url) # Download file from VK - logger.info('Image saved locally as: %s' % tmpfile) + log.info('Image saved locally as: %s' % tmpfile) media_id = json.loads(post_media(tmpfile).text)['id'] # Upload file to Mastodon media_ids.append(media_id) # Save uploaded media IDs - logger.info('Remove local file: %s' % tmpfile) + log.info('Remove local file: %s' % tmpfile) os.remove(tmpfile) # Remove local file i += 1 @@ -167,7 +168,8 @@ def post_toot(post_data: dict) -> str: # Get status text text = post_text + '\n\n' + 'Source: ' + vk_post_url + '\n\nAttachments:\n' + post_attachments - + print('Длина текста', len(text)) + print(text) # Post toot! publish_toot(text, media_ids) @@ -182,15 +184,15 @@ def read_lock_file(file: str): return data['post_id'] def poll(): - logger.info('Start polling %s' % datetime.datetime.now().isoformat()) + log.info('Start polling %s' % datetime.datetime.now().isoformat()) lock_file = './data/post_id.json' if os.path.exists(lock_file): prev_post_id = read_lock_file(lock_file) - logger.info('Read last post ID from file: %s' % lock_file) - logger.info('Last post ID: %s' % prev_post_id) + log.info('Read last post ID from file: %s' % lock_file) + log.info('Last post ID: %s' % prev_post_id) else: prev_post_id = 0 - logger.info('Last post ID: 0') + log.info('Last post ID: 0') while True: post_data = get_vk_group_last_post() # raw data @@ -198,11 +200,12 @@ def poll(): # Don't post duplicates if int(post_id) == int(prev_post_id): - logger.info('Post with VK ID %s already posted, skipping' % post_id) + log.info('Post with VK ID %s already posted, skipping' % post_id) else: # Toot! - logger.info('Toot! VK post ID: %s' % post_id) - post_toot(post_data) + log.info('Toot! VK post ID: {}, URL: {}'.format( + post_id, get_vk_post_url(post_data))) + toot(post_data) touch_lock_file(lock_file, post_id) prev_post_id = read_lock_file(lock_file) @@ -215,4 +218,9 @@ def poll(): if __name__ == '__main__': + log.info('VK-Toot started. [bridge]: {}; [mastodon].API_URL {}'.format( + dict(config['bridge']), + config['mastodon']['API_URL'], + )) + # Start polling poll()