Bump version to 0.2.1. See README
This commit is contained in:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
								
							@@ -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-х штук). Только изображения.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
		Reference in New Issue
	
	Block a user