Bump version to 0.2.1. See README

This commit is contained in:
ge 2022-07-07 09:01:14 +03:00
parent 09e6e40744
commit cda10fde05
3 changed files with 69 additions and 31 deletions

View File

@ -4,4 +4,4 @@ RUN mkdir -p /opt/vk-mastodon-bridge/data
ADD . /opt/vk-mastodon-bridge ADD . /opt/vk-mastodon-bridge
WORKDIR /opt/vk-mastodon-bridge WORKDIR /opt/vk-mastodon-bridge
RUN pip install --upgrade pip && pip install --requirement requirements.txt RUN pip install --upgrade pip && pip install --requirement requirements.txt
CMD source .env; python3 vk-mastodon-bridge.py CMD source .env; python3 vk_mastodon_bridge.py

View File

@ -10,10 +10,10 @@
## Известные проблемы/TODO ## Известные проблемы/TODO
- Скрипт пропускает часть постов. Пока не выяснил почему.
- Добавить логирование.
- Не учитывается длина поста. Если исходный пост не будет укладываться в лимит символов на инстансе Mastodon'а, то неизвестно что произойдёт. Решение: надо обрезать текст поста в функции `build_post()`. - Не учитывается длина поста. Если исходный пост не будет укладываться в лимит символов на инстансе Mastodon'а, то неизвестно что произойдёт. Решение: надо обрезать текст поста в функции `build_post()`.
- Никак не обрабатываются вложения типов отличных от фото (`photo`) и фотоальбома (`album`). - Никак не обрабатываются вложения типов отличных от фото (`photo`) и фотоальбома (`album`).
- Указывать в списке вложений в посте только те вложения, которые не были загружены в Mastodon. Пока формируется полный список вложений.
- Использовать конфиг вместо переменных окружения?
## Настройки и запуск ## Настройки и запуск
@ -39,15 +39,16 @@
Нужно задать все переменные окружения. Удобный способ — в файле `.env` и экспортировать в шэлл. Нужно задать все переменные окружения. Удобный способ — в файле `.env` и экспортировать в шэлл.
| Переменная окружения | Пример | Описание | | Переменная окружения | Умолчание | Пример | Описание |
| ----------------------------- | --------------------------------- | ------------------------------------------------------------- | | ----------------------------- | --------- |---------------------------------- | ---------------------------------------------- |
| `MASTODON_API_URL` | https://mastodon.social/api/v1 | URL адрес API Mastodon. | | `MASTODON_API_URL` | нет | https://mastodon.social/api/v1 | URL адрес API Mastodon. |
| `MASTODON_API_ACCESS_TOKEN` | | Ключ API Mastodon. | | `MASTODON_API_ACCESS_TOKEN` | нет | | Ключ API Mastodon. |
| `VK_API_URL` | https://api.vk.com/method | URL API VK. | | `VK_API_URL` | нет | https://api.vk.com/method | URL API VK. |
| `VK_API_VERSION` | 5.131 | Версия VK API. | | `VK_API_VERSION` | нет | 5.131 | Версия VK API. |
| `VK_API_ACCESS_TOKEN` | | Ключ API VK. | | `VK_API_ACCESS_TOKEN` | нет | | Ключ API VK. |
| `VK_GROUP_DOMAIN` | apiclub | slug адрес группы/паблика VK. | | `VK_GROUP_DOMAIN` | нет | apiclub | slug адрес группы/паблика VK. |
| `POLLING_TIME` | 300 | Количество секунд между получением постов. Умолчание: 300. | | `POLLING_TIME` | 300 | 300 | Задержка получением постов из VK в секундах. |
| `REQUEST_DELAY` | 1 | 1 | Задержка загрузки медиа в Mastodon в секундах. |
### Запуск без Docker ### Запуск без Docker
@ -64,7 +65,7 @@ python vk-mastodon-bridge.py
Сборка образа: Сборка образа:
``` ```
sudo docker build -t vk-mastodon-bridge:0.2.0 . sudo docker build -t vk-mastodon-bridge:0.2.1 .
``` ```
Запуск контейнера: Запуск контейнера:
@ -73,11 +74,19 @@ sudo docker build -t vk-mastodon-bridge:0.2.0 .
sudo docker run --detach \ sudo docker run --detach \
--name vk-mastodon-bridge \ --name vk-mastodon-bridge \
--volume /opt/vk-mastodon-bridge/data:/opt/vk-mastodon-bridge/data \ --volume /opt/vk-mastodon-bridge/data:/opt/vk-mastodon-bridge/data \
vk-mastodon-bridge:0.2.0 vk-mastodon-bridge:0.2.1
``` ```
## История изменений ## История изменений
### 0.2.1
- Исправлена публиация постов. Mastodon API мог отвечать кодом 429 из-за слишком частых загрузок медиа. Решено через добавление задержки `REQUEST_DELAY`.
- Исправлена ошибка при посте статуса с менее чем 4-я вложениями.
- Файл `./data/last_post_id` теперь содержит JSON и называется `./data/post_id.json`. ID хранится в единственном поле `post_id`.
- Имя модуля теперь не содержит дефисов.
- Добавлено логирование. Лог пишется в STDOUT.
### 0.2.0 ### 0.2.0
- Реализована загрузка вложений в Mastodon (до 4-х штук). Только изображения. - Реализована загрузка вложений в Mastodon (до 4-х штук). Только изображения.

View File

@ -1,10 +1,13 @@
__version__ = '0.2.0' __version__ = '0.2.1'
import os import os
import sys
import json import json
import time import time
import datetime
import logging
import shutil import shutil
from urllib.parse import urlparse import urllib.parse
import requests import requests
@ -16,6 +19,13 @@ VK_API_VERSION = os.environ['VK_API_VERSION']
VK_API_ACCESS_TOKEN = os.environ['VK_API_ACCESS_TOKEN'] VK_API_ACCESS_TOKEN = os.environ['VK_API_ACCESS_TOKEN']
VK_GROUP_DOMAIN = os.environ['VK_GROUP_DOMAIN'] VK_GROUP_DOMAIN = os.environ['VK_GROUP_DOMAIN']
# Set up logger
logger = logging.getLogger('vk_mastodon_bridge')
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(stream=sys.stdout)
handler.setFormatter(logging.Formatter(fmt = '[%(asctime)s: %(levelname)s] %(message)s'))
logger.addHandler(handler)
def get_vk_group_last_post(): def get_vk_group_last_post():
"""Return dict with VK group last post data.""" """Return dict with VK group last post data."""
return json.loads(requests.get(VK_API_URL + '/wall.get' \ return json.loads(requests.get(VK_API_URL + '/wall.get' \
@ -81,7 +91,7 @@ def get_vk_post_attachments(post_data: dict) -> list:
def download_file(url: str) -> str: def download_file(url: str) -> str:
"""Save file to /tmp. Return file path""" """Save file to /tmp. Return file path"""
filename = '/tmp/' + os.path.basename(urlparse(url).path) filename = '/tmp/' + os.path.basename(urllib.parse.urlparse(url).path)
response = requests.get(url, stream=True) response = requests.get(url, stream=True)
with open(filename, 'wb') as out_file: with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file) shutil.copyfileobj(response.raw, out_file)
@ -93,6 +103,14 @@ def post_media(file: str) -> str:
headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN} headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN}
files = {'file': open(file,'rb')} files = {'file': open(file,'rb')}
response = requests.post(MASTODON_API_URL + '/media', files=files, headers=headers) response = requests.post(MASTODON_API_URL + '/media', files=files, headers=headers)
logger.info('Post media on Mastodon. Response: ' \
+ str(response.status_code) + ' ' + str(response.text))
# Sleep some seconds to prevent HTTP 429 response.
try:
request_delay = int(os.environ['REQUEST_DELAY'])
except (KeyError, TypeError):
request_delay = 1
time.sleep(request_delay)
return response return response
def publish_toot(post_text: str, media_ids: list): def publish_toot(post_text: str, media_ids: list):
@ -102,6 +120,9 @@ def publish_toot(post_text: str, media_ids: list):
headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN} headers = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN}
params = {'status': post_text, 'media_ids[]': media_ids} params = {'status': post_text, 'media_ids[]': media_ids}
response = requests.post(MASTODON_API_URL + '/statuses', data=params, headers=headers) response = requests.post(MASTODON_API_URL + '/statuses', data=params, headers=headers)
post_url = json.loads(response.text)['url']
logger.info('Publish status. Response: ' \
+ str(response.status_code) + ' Status: ' + str(post_url))
return response return response
def post_toot(post_data: dict) -> str: def post_toot(post_data: dict) -> str:
@ -115,17 +136,21 @@ def post_toot(post_data: dict) -> str:
# Upload only first 4 photos and get media_ids # Upload only first 4 photos and get media_ids
i = 0 i = 0
media_ids = [] media_ids = []
while i < 4: logger.info('Attachments: %s' % str(attachments))
attachments_count = len(attachments)
if attachments_count > 4:
attachments_count = 4
while i < attachments_count:
if list(attachments[i].keys())[0] == 'photo': if list(attachments[i].keys())[0] == 'photo':
photo_url = attachments[i]['photo'] photo_url = attachments[i]['photo']
print('Download image:', photo_url) # Log logger.info('Download image: %s' % photo_url)
tmpfile = download_file(photo_url) # Download file from VK tmpfile = download_file(photo_url) # Download file from VK
print('Saved as:', tmpfile) # Log logger.info('Image saved locally as: %s' % tmpfile)
print('Upload to Mastodon:', tmpfile) # Log
media_id = json.loads(post_media(tmpfile).text)['id'] # Upload file to Mastodon media_id = json.loads(post_media(tmpfile).text)['id'] # Upload file to Mastodon
print('Uploaded. Media ID:', media_id) # Log
media_ids.append(media_id) # Save uploaded media IDs media_ids.append(media_id) # Save uploaded media IDs
print('Remove local file:', tmpfile) # Log logger.info('Remove local file: %s' % tmpfile)
os.remove(tmpfile) # Remove local file os.remove(tmpfile) # Remove local file
i += 1 i += 1
@ -144,31 +169,35 @@ def post_toot(post_data: dict) -> str:
def touch_lock_file(file: str, post_id: int): def touch_lock_file(file: str, post_id: int):
with open(file, 'w') as lock: with open(file, 'w') as lock:
lock.write(str(post_id)) data = json.dumps({'post_id': str(post_id)})
lock.write(data)
def read_lock_file(file: str): def read_lock_file(file: str):
with open(file, 'r') as lock: with open(file, 'r') as lock:
return int(lock.read()) data = json.loads(lock.read())
return data['post_id']
def poll(): def poll():
lock_file = './data/last_post_id' logger.info('Start polling %s' % datetime.datetime.now().isoformat())
lock_file = './data/post_id.json'
if os.path.exists(lock_file): if os.path.exists(lock_file):
prev_post_id = read_lock_file(lock_file) prev_post_id = read_lock_file(lock_file)
print('Read last post ID from file:', lock_file) logger.info('Read last post ID from file: %s' % lock_file)
logger.info('Last post ID: %s' % prev_post_id)
else: else:
prev_post_id = 0 prev_post_id = 0
print('Last post ID is 0') logger.info('Last post ID: 0')
while True: while True:
post_data = get_vk_group_last_post() # raw data post_data = get_vk_group_last_post() # raw data
post_id = post_data['response']['items'][0]['id'] post_id = post_data['response']['items'][0]['id']
# Don't post duplicates # Don't post duplicates
if post_id == prev_post_id: if int(post_id) == int(prev_post_id):
print('Skip posting') # Log logger.info('Post with VK ID %s already posted, skipping' % post_id)
else: else:
# Toot! # Toot!
print('===> Toot! VK post ID:', post_id) # Log logger.info('Toot! VK post ID: %s' % post_id)
post_toot(post_data) post_toot(post_data)
touch_lock_file(lock_file, post_id) touch_lock_file(lock_file, post_id)