init
This commit is contained in:
commit
12134e4bb8
6
.env.example
Normal file
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
||||
export MASTODON_API_URL='https://mastodon.social/api/v1'
|
||||
export MASTODON_API_ACCESS_TOKEN='your access token here'
|
||||
export VK_API_URL='https://api.vk.com/method'
|
||||
export VK_API_VERSION='5.131'
|
||||
export VK_API_ACCESS_TOKEN='your access token here'
|
||||
export VK_GROUP_DOMAIN='apiclub'
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM alpine:latest
|
||||
RUN apk update && apk add python3 py3-pip
|
||||
RUN mkdir -p /vk-mastodon-bridge
|
||||
ADD . /vk-mastodon-bridge
|
||||
WORKDIR /vk-mastodon-bridge
|
||||
RUN pip install --upgrade pip && pip install --requirement requirements.txt
|
||||
CMD source .env; python3 vk-mastodon-bridge.py
|
91
README.rst
Normal file
91
README.rst
Normal file
@ -0,0 +1,91 @@
|
||||
==================
|
||||
vk-mastodon-bridge
|
||||
==================
|
||||
|
||||
Бридж для кросспостинга из `VK`_ в `Mastodon`_. Это приложение будет дублировать посты из паблика или публичной группы VK в аккаунт Mastodon.
|
||||
|
||||
Пример аккаунта бота: `@jrokku@mas.to`_
|
||||
|
||||
.. _VK: https://vk.com/
|
||||
.. _Mastodon: https://joinmastodon.org/
|
||||
.. _jrokku@mas.to: https://mas.to/@jrokku
|
||||
|
||||
|
||||
Как это работает
|
||||
================
|
||||
|
||||
Скрипт согласно заданному промежутку времени (см. ниже ``POLLING_TIME``) ходит в API VK и забирает оттуда самый свежий пост, запоминает его ID в файл. Затем ходит в API Mastodon и создаёт новый пост, который содержит оригинальный текст поста, ссылку на оригинальный пост, ссылки на каждое вложение из оригинального поста.
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
- Не учитывается длина поста. Если исходный пост не будет укладываться в лимит символов на инстансе Mastodon'а, то неизвестно что произойдёт. Решение: надо обрезать текст поста в функции ``build_post()``.
|
||||
- Никак не обрабатываются вложения типов отличных от фото (`photo`) и фотоальбома (`album`).
|
||||
- Надо первые 4-е картинки добавлять как вложения в пост в мастодоне. Для остальных вложений достаточно ссылки.
|
||||
|
||||
Настройки и запуск
|
||||
==================
|
||||
|
||||
API Mastodon
|
||||
------------
|
||||
|
||||
Для получения токена API Mastodon достаточно добавить приложение в своём профиле (Preferences -> Development). Из разрешений достаточно `write` для создания постов, ничего другого бридж делать не умеет.
|
||||
|
||||
API VK
|
||||
------
|
||||
|
||||
Нужно добавить приложение на https://dev.vk.com. Будет получен ``APP_ID``.
|
||||
|
||||
Для получения бессрочного токена перейти по ссылке следующего вида::
|
||||
|
||||
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`` задаёт нулевой (бесконечный) срок жизни токена.
|
||||
|
||||
При переходе по ссылке произойдёт редирект на страницу с подтверждением выдачи доступа приложению и далее редирект на страницу с предупреждением. Нас интересует значение query-параметра ``access_token`` в адресной строке.
|
||||
|
||||
Это всё также описано где-то в документации к API: https://dev.vk.com/api/access-token/implicit-flow-user
|
||||
|
||||
Переменные окружения
|
||||
--------------------
|
||||
|
||||
Нужно задать все переменные окружения. Удобный способ — в файле ``.env`` и экспортировать в шэлл.
|
||||
|
||||
========================= =============================== ========================================
|
||||
Переменная окружения Пример Описание
|
||||
========================= =============================== ========================================
|
||||
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.
|
||||
POLLING_TIME 300 Количество секунд между получением
|
||||
постов. По умолчанию 300.
|
||||
========================= =============================== ========================================
|
||||
|
||||
Запуск
|
||||
------
|
||||
|
||||
::
|
||||
|
||||
pip -r requirements.txt
|
||||
source .env
|
||||
python vk-mastodon-bridge.py
|
||||
|
||||
Docker
|
||||
------
|
||||
|
||||
::
|
||||
|
||||
sudo docker build -t vk-mastodon-bridge .
|
||||
sudo docker run --detach --name vkmbridge vk-mastodon-bridge
|
||||
|
||||
|
||||
История изменений
|
||||
=================
|
||||
|
||||
0.1.0
|
||||
-----
|
||||
|
||||
Initial release.
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
requests
|
142
vk-mastodon-bridge.py
Normal file
142
vk-mastodon-bridge.py
Normal file
@ -0,0 +1,142 @@
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
MASTODON_API_URL = os.environ['MASTODON_API_URL']
|
||||
MASTODON_API_ACCESS_TOKEN = os.environ['MASTODON_API_ACCESS_TOKEN']
|
||||
VK_API_URL = os.environ['VK_API_URL']
|
||||
VK_API_VERSION = os.environ['VK_API_VERSION']
|
||||
VK_API_ACCESS_TOKEN = os.environ['VK_API_ACCESS_TOKEN']
|
||||
VK_GROUP_DOMAIN = os.environ['VK_GROUP_DOMAIN']
|
||||
|
||||
def get_vk_group_last_post():
|
||||
"""Return dict with VK group last post data."""
|
||||
return json.loads(requests.get(VK_API_URL + '/wall.get' \
|
||||
+ '?v=' + VK_API_VERSION \
|
||||
+ '&access_token=' + VK_API_ACCESS_TOKEN \
|
||||
+ '&domain=' + VK_GROUP_DOMAIN \
|
||||
+ '&count=1').text)
|
||||
|
||||
def get_vk_post_text(post_data: dict) -> str:
|
||||
"""See: https://dev.vk.com/reference/objects/post"""
|
||||
return post_data['response']['items'][0]['text']
|
||||
|
||||
def get_vk_post_url(post_data: dict) -> str:
|
||||
"""Return link to original post on vk.com
|
||||
See: https://dev.vk.com/reference/objects/post
|
||||
"""
|
||||
wall_id = str(post_data['response']['items'][0]['owner_id'])
|
||||
post_id = str(post_data['response']['items'][0]['id'])
|
||||
return 'https://vk.com/wall' + wall_id + '_' + post_id
|
||||
|
||||
def get_vk_post_attachments(post_data: dict) -> list:
|
||||
"""Process attachments. See attachments at
|
||||
https://dev.vk.com/method/wall.post
|
||||
Return list with following structure::
|
||||
|
||||
[{'photo': 'url'}, {'album': 'url'}]
|
||||
"""
|
||||
attachments = []
|
||||
raw_attachments = post_data['response']['items'][0]['attachments']
|
||||
|
||||
for attachment in raw_attachments:
|
||||
if attachment['type'] == 'photo':
|
||||
# Get photo in max size by height (photos are proportionally resized)
|
||||
height = [ photo['height'] for photo in attachment['photo']['sizes'] ]
|
||||
for photo in attachment['photo']['sizes']:
|
||||
if photo['height'] == max(height):
|
||||
photo_url = photo['url']
|
||||
attachments.append({'photo': photo_url})
|
||||
elif attachment['type'] == 'video':
|
||||
pass
|
||||
elif attachment['type'] == 'audio':
|
||||
pass
|
||||
elif attachment['type'] == 'doc':
|
||||
pass
|
||||
elif attachment['type'] == 'page':
|
||||
pass
|
||||
elif attachment['type'] == 'note':
|
||||
pass
|
||||
elif attachment['type'] == 'pull':
|
||||
pass
|
||||
elif attachment['type'] == 'album':
|
||||
owner_id = str(attachment['album']['owner_id'])
|
||||
id = str(attachment['album']['id'])
|
||||
album_url = 'https://vk.com/album' + owner_id + '_' + id
|
||||
attachments.append({'album': album_url})
|
||||
elif attachment['type'] == 'market':
|
||||
pass
|
||||
elif attachment['type'] == 'market_album':
|
||||
pass
|
||||
elif attachment['type'] == 'audio_playlist':
|
||||
pass
|
||||
return attachments
|
||||
|
||||
def build_post(post_data: dict) -> str:
|
||||
post_text = get_vk_post_text(post_data)
|
||||
vk_post_url = get_vk_post_url(post_data)
|
||||
attachments = get_vk_post_attachments(post_data)
|
||||
|
||||
# Build attachments list
|
||||
post_attachments = ''
|
||||
for attachment in attachments:
|
||||
key = str(list(attachment.keys())[0])
|
||||
# Example resulting string: 'Album: https://vk.com/album-26788782_284176934'
|
||||
post_attachments = post_attachments + key.title() + ': ' + attachment[key] + '\n'
|
||||
return post_text + '\n\n' + 'Source: ' + vk_post_url + '\n\nAttachments:\n' + post_attachments
|
||||
|
||||
def post_toot(post_text: str):
|
||||
"""Post toot on Mastodon.
|
||||
See: https://docs.joinmastodon.org/methods/statuses/
|
||||
"""
|
||||
auth = {'Authorization': 'Bearer ' + MASTODON_API_ACCESS_TOKEN}
|
||||
params = {'status': post_text}
|
||||
response = requests.post(MASTODON_API_URL + '/statuses', data=params, headers=auth)
|
||||
return response
|
||||
|
||||
def touch_lock_file(file: str, post_id: int):
|
||||
with open(file, 'w') as lock:
|
||||
lock.write(str(post_id))
|
||||
|
||||
def read_lock_file(file: str):
|
||||
with open(file, 'r') as lock:
|
||||
return int(lock.read())
|
||||
|
||||
def poll():
|
||||
lock_file = './.last_post_id.tmp'
|
||||
if os.path.exists(lock_file):
|
||||
prev_post_id = read_lock_file(lock_file)
|
||||
print('Read last post ID from file:', lock_file)
|
||||
else:
|
||||
prev_post_id = 0
|
||||
print('Last post ID is 0')
|
||||
|
||||
while True:
|
||||
post_data = get_vk_group_last_post() # raw data
|
||||
post_id = post_data['response']['items'][0]['id']
|
||||
|
||||
# Don't post duplicates
|
||||
if post_id == prev_post_id:
|
||||
print('Skip posting')
|
||||
else:
|
||||
# Toot!
|
||||
print('Toot! VK post ID:', post_id)
|
||||
post = build_post(post_data)
|
||||
post_toot(post)
|
||||
|
||||
touch_lock_file(lock_file, post_id)
|
||||
prev_post_id = read_lock_file(lock_file)
|
||||
try:
|
||||
poll_time = int(os.environ['POLLING_TIME'])
|
||||
except (KeyError, TypeError):
|
||||
poll_time = 300
|
||||
|
||||
time.sleep(poll_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
poll()
|
Reference in New Issue
Block a user