add files
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
data/*
|
||||
logs/*
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем зависимости для requests (SSL и т.п.)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Рабочая директория
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем зависимости
|
||||
COPY requirements.txt .
|
||||
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем сам скрипт
|
||||
COPY tg_to_vk.py .
|
||||
|
||||
# Файл для хранения последнего update_id
|
||||
RUN touch last_tg_id.txt
|
||||
|
||||
# Запускаем
|
||||
CMD ["python", "tg_to_vk.py"]
|
||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
tg-to-vk:
|
||||
build: .
|
||||
container_name: tg_to_vk
|
||||
restart: unless-stopped
|
||||
command: python -u tg_to_vk.py
|
||||
volumes:
|
||||
- ./data:/app/data # Для хранения last_id.txt
|
||||
- ./logs:/app/logs # Для хранения логов
|
||||
environment:
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- TELEGRAM_CHANNEL=${TELEGRAM_CHANNEL}
|
||||
- VK_ACCESS_TOKEN=${VK_ACCESS_TOKEN}
|
||||
- VK_GROUP_ID=${VK_GROUP_ID}
|
||||
- CHECK_INTERVAL=${CHECK_INTERVAL}
|
||||
|
||||
|
||||
|
||||
#curl "https://api.telegram.org/bot8244995454:AAFREqyqmZFq0OfqhFku_rxzG1jJzzopKec/getUpdates"
|
||||
#curl "https://api.vk.com/method/wall.post?owner_id=-14409962&access_token=vk1.a.OJzpeLyBRLe6RSh5dIFwzVqW-DN55yMHrVBPmJ6OXfrxceE_XXT7qrTv0gonQP8WHbJD9WefwxOYp39ZHaO5XK4blQMxMvpiwkQtpM9W3pE58GhSmHb1V3jIvZoTlVRRQzcvsRJ6Ps5MC9cp1Z-ix7C7b1lwK7_Gh36xAH__VfRWiP_yMzDEuTYRiZvPUM9yMKZQviuOsTQob7YjqKwG4w"&v=5.131"
|
||||
#an_tg_vk t.me/an_tg2vk_bot.
|
||||
#https://oauth.vk.com/blank.html#access_token=vk1.a.OJzpeLyBRLe6RSh5dIFwzVqW-DN55yMHrVBPmJ6OXfrxceE_XXT7qrTv0gonQP8WHbJD9WefwxOYp39ZHaO5XK4blQMxMvpiwkQtpM9W3pE58GhSmHb1V3jIvZoTlVRRQzcvsRJ6Ps5MC9cp1Z-ix7C7b1lwK7_Gh36xAH__VfRWiP_yMzDEuTYRiZvPUM9yMKZQviuOsTQob7YjqKwG4w&expires_in=0&user_id=59992304&email=prytkov2005@yandex.ru
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests
|
||||
python-dotenv>=1.0.0
|
||||
python-telegram-bot==13.15
|
||||
16
test_log.py
Normal file
16
test_log.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler("bot.log", encoding="utf-8"),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(10):
|
||||
logging.info(f"Тестовое сообщение {i}")
|
||||
time.sleep(1)
|
||||
355
tg_to_vk.py
Normal file
355
tg_to_vk.py
Normal file
@@ -0,0 +1,355 @@
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Инициализация путей
|
||||
BASE_DIR = Path(__file__).parent
|
||||
DATA_DIR = BASE_DIR / "data"
|
||||
LOG_DIR = BASE_DIR / "logs"
|
||||
|
||||
# Создание директорий
|
||||
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
||||
LOG_DIR.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Загрузка переменных окружения
|
||||
load_dotenv()
|
||||
|
||||
# Конфигурация
|
||||
TELEGRAM_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||
TELEGRAM_CHANNEL = os.getenv('TELEGRAM_CHANNEL')
|
||||
VK_TOKEN = os.getenv('VK_ACCESS_TOKEN')
|
||||
VK_GROUP_ID = int(os.getenv('VK_GROUP_ID', '-14409962'))
|
||||
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '180'))
|
||||
|
||||
# Логирование
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_DIR / 'vk_crosspost.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('VK_CrossPost')
|
||||
|
||||
class TelegramToVK:
|
||||
def __init__(self):
|
||||
self.MAX_ATTACHMENTS = 10
|
||||
self.LAST_ID_FILE = DATA_DIR / "last_id.txt"
|
||||
self.last_id = self._load_last_id()
|
||||
logger.info(f"Bot initialized. Last processed ID: {self.last_id}")
|
||||
|
||||
def _load_last_id(self):
|
||||
"""Загрузка последнего ID с созданием файла при необходимости"""
|
||||
try:
|
||||
if self.LAST_ID_FILE.exists():
|
||||
with open(self.LAST_ID_FILE, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if content.isdigit():
|
||||
return int(content)
|
||||
|
||||
# Если файла нет или он пуст/поврежден
|
||||
last_id = self._fetch_last_post_id()
|
||||
self._save_last_id(last_id)
|
||||
return last_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading last_id: {e}")
|
||||
return 0
|
||||
|
||||
def _fetch_last_post_id(self):
|
||||
"""Получение последнего ID поста из Telegram"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f'https://api.telegram.org/bot{TELEGRAM_TOKEN}/getUpdates',
|
||||
params={'limit': 1},
|
||||
timeout=10
|
||||
).json()
|
||||
|
||||
if response.get('result'):
|
||||
return response['result'][-1]['update_id']
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching last post ID: {e}")
|
||||
return 0
|
||||
|
||||
def _save_last_id(self, last_id):
|
||||
"""Атомарное сохранение ID последнего поста"""
|
||||
try:
|
||||
temp_file = self.LAST_ID_FILE.with_suffix('.tmp')
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(str(last_id))
|
||||
temp_file.replace(self.LAST_ID_FILE)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save last_id: {e}")
|
||||
|
||||
def _get_telegram_updates(self):
|
||||
"""Получение новых постов"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f'https://api.telegram.org/bot{TELEGRAM_TOKEN}/getUpdates',
|
||||
params={
|
||||
'offset': self.last_id + 1,
|
||||
'allowed_updates': ['channel_post'],
|
||||
'timeout': 30
|
||||
},
|
||||
timeout=35
|
||||
).json()
|
||||
|
||||
if not response.get('ok'):
|
||||
logger.error(f"Telegram API error: {response.get('description')}")
|
||||
return []
|
||||
|
||||
return response.get('result', [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting updates: {e}")
|
||||
return []
|
||||
|
||||
def _process_media(self, post):
|
||||
"""Обработка медиа без дублирования"""
|
||||
attachments = []
|
||||
|
||||
# Обработка видео
|
||||
if 'video' in post:
|
||||
video = post['video']
|
||||
if attachment := self._upload_media(video['file_id'], 'video'):
|
||||
attachments.append(attachment)
|
||||
logger.debug("Added video attachment")
|
||||
|
||||
# Обработка фото (только самое качественное)
|
||||
if 'photo' in post and len(post['photo']) > 0:
|
||||
best_photo = post['photo'][-1]
|
||||
if attachment := self._upload_media(best_photo['file_id'], 'photo'):
|
||||
attachments.append(attachment)
|
||||
logger.debug(f"Added photo (best of {len(post['photo'])} versions)")
|
||||
|
||||
return attachments[:self.MAX_ATTACHMENTS]
|
||||
|
||||
def _upload_media(self, file_id, media_type):
|
||||
"""Загрузка медиа в VK"""
|
||||
try:
|
||||
file_url = self._get_telegram_file_url(file_id)
|
||||
if not file_url:
|
||||
return None
|
||||
|
||||
if media_type == 'photo':
|
||||
# Получение URL для загрузки фото
|
||||
upload_server = requests.get(
|
||||
'https://api.vk.com/method/photos.getWallUploadServer',
|
||||
params={
|
||||
'group_id': abs(VK_GROUP_ID),
|
||||
'access_token': VK_TOKEN,
|
||||
'v': '5.131'
|
||||
},
|
||||
timeout=10
|
||||
).json()
|
||||
|
||||
if 'error' in upload_server:
|
||||
logger.error(f"VK upload server error: {upload_server['error']}")
|
||||
return None
|
||||
|
||||
# Загрузка фото
|
||||
file_data = requests.get(file_url, timeout=10).content
|
||||
upload = requests.post(
|
||||
upload_server['response']['upload_url'],
|
||||
files={'photo': ('image.jpg', file_data)},
|
||||
timeout=15
|
||||
).json()
|
||||
|
||||
# Сохранение фото
|
||||
save_response = requests.get(
|
||||
'https://api.vk.com/method/photos.saveWallPhoto',
|
||||
params={
|
||||
'group_id': abs(VK_GROUP_ID),
|
||||
'photo': upload['photo'],
|
||||
'server': upload['server'],
|
||||
'hash': upload['hash'],
|
||||
'access_token': VK_TOKEN,
|
||||
'v': '5.131'
|
||||
},
|
||||
timeout=10
|
||||
).json()
|
||||
|
||||
if 'error' in save_response:
|
||||
logger.error(f"VK save photo error: {save_response['error']}")
|
||||
return None
|
||||
|
||||
photo = save_response['response'][0]
|
||||
return f"photo{photo['owner_id']}_{photo['id']}"
|
||||
|
||||
elif media_type == 'video':
|
||||
# Загрузка видео
|
||||
upload_server = requests.get(
|
||||
'https://api.vk.com/method/video.save',
|
||||
params={
|
||||
'name': 'Video from Telegram',
|
||||
'group_id': abs(VK_GROUP_ID),
|
||||
'access_token': VK_TOKEN,
|
||||
'v': '5.131'
|
||||
},
|
||||
timeout=10
|
||||
).json()
|
||||
|
||||
if 'error' in upload_server:
|
||||
logger.error(f"VK video save error: {upload_server['error']}")
|
||||
return None
|
||||
|
||||
file_data = requests.get(file_url, timeout=15).content
|
||||
upload = requests.post(
|
||||
upload_server['response']['upload_url'],
|
||||
files={'video_file': ('video.mp4', file_data)},
|
||||
timeout=20
|
||||
).json()
|
||||
|
||||
return f"video{upload['owner_id']}_{upload['video_id']}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading {media_type}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _get_telegram_file_url(self, file_id):
|
||||
"""Получение URL файла из Telegram"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f'https://api.telegram.org/bot{TELEGRAM_TOKEN}/getFile',
|
||||
params={'file_id': file_id},
|
||||
timeout=10
|
||||
).json()
|
||||
|
||||
if response.get('ok'):
|
||||
return f"https://api.telegram.org/file/bot{TELEGRAM_TOKEN}/{response['result']['file_path']}"
|
||||
logger.error(f"Telegram file error: {response.get('description')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file URL: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _post_to_vk(self, text, attachments):
|
||||
"""Публикация поста в VK"""
|
||||
if not text and not attachments:
|
||||
logger.warning("Skipping empty post")
|
||||
return False
|
||||
|
||||
params = {
|
||||
'owner_id': VK_GROUP_ID,
|
||||
'from_group': 1,
|
||||
'access_token': VK_TOKEN,
|
||||
'v': '5.131'
|
||||
}
|
||||
|
||||
if text:
|
||||
params['message'] = text[:1000] # Лимит VK
|
||||
logger.debug(f"Post text: {text[:50]}...")
|
||||
|
||||
if attachments:
|
||||
params['attachments'] = ','.join(attachments[:10]) # Лимит VK
|
||||
logger.debug(f"Attachments: {len(attachments)} items")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://api.vk.com/method/wall.post',
|
||||
params=params,
|
||||
timeout=15
|
||||
).json()
|
||||
|
||||
if 'error' in response:
|
||||
logger.error(f"VK API error: {response['error']}")
|
||||
return False
|
||||
|
||||
post_id = response['response']['post_id']
|
||||
logger.info(f"Posted: https://vk.com/wall{VK_GROUP_ID}_{post_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error posting to VK: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
"""Основной цикл работы"""
|
||||
logger.info("Starting Telegram to VK crossposting bot...")
|
||||
try:
|
||||
while True:
|
||||
self._process_new_posts()
|
||||
logger.debug(f"Waiting {CHECK_INTERVAL} seconds...")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
except Exception as e:
|
||||
logger.critical(f"Fatal error: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _process_new_posts(self):
|
||||
updates = self._get_telegram_updates()
|
||||
if not updates:
|
||||
logger.debug("No new updates")
|
||||
return
|
||||
|
||||
logger.info(f"Found {len(updates)} new updates")
|
||||
|
||||
# Группировка по media_group_id (альбом) или update_id (одиночный пост)
|
||||
grouped_posts = {}
|
||||
for update in sorted(updates, key=lambda x: x['update_id']):
|
||||
current_id = update['update_id']
|
||||
if current_id <= self.last_id:
|
||||
continue
|
||||
|
||||
post = update.get('channel_post')
|
||||
if not post:
|
||||
continue
|
||||
|
||||
group_id = post.get('media_group_id', current_id) # альбом или одиночка
|
||||
grouped_posts.setdefault(group_id, []).append((current_id, post))
|
||||
|
||||
# Обработка каждой группы постов
|
||||
for group_id, posts in grouped_posts.items():
|
||||
posts.sort(key=lambda x: x[0]) # сортировка внутри группы по update_id
|
||||
|
||||
# Текст берём из первого поста группы
|
||||
first_post = posts[0][1]
|
||||
text = first_post.get('text') or first_post.get('caption') or ""
|
||||
|
||||
attachments = []
|
||||
|
||||
for _, post in posts:
|
||||
# Фото (берём самое качественное из набора размеров)
|
||||
photos = post.get('photo') or []
|
||||
if photos:
|
||||
best_photo = photos[-1]
|
||||
att = self._upload_media(best_photo['file_id'], 'photo')
|
||||
if att:
|
||||
attachments.append(att)
|
||||
|
||||
# Видео
|
||||
if 'video' in post:
|
||||
att = self._upload_media(post['video']['file_id'], 'video')
|
||||
if att:
|
||||
attachments.append(att)
|
||||
|
||||
# Ограничим количество вложений (и так в _post_to_vk есть [:10], но пусть будет явно)
|
||||
if attachments:
|
||||
attachments = attachments[:self.MAX_ATTACHMENTS]
|
||||
|
||||
# Публикуем, если есть текст ИЛИ вложения.
|
||||
if text or attachments:
|
||||
if self._post_to_vk(text, attachments):
|
||||
self.last_id = posts[-1][0] # помечаем обработанным последний update из группы
|
||||
self._save_last_id(self.last_id)
|
||||
logger.info(f"Processed group {group_id} (text={'yes' if text else 'no'}, attachments={len(attachments)})")
|
||||
else:
|
||||
logger.error(f"Failed to post group {group_id}")
|
||||
else:
|
||||
# Совсем пустая группа (редко, но бывает) — помечаем как обработанную, чтобы не зациклиться
|
||||
self.last_id = posts[-1][0]
|
||||
self._save_last_id(self.last_id)
|
||||
logger.info(f"Skipped empty group {group_id}: no text and no media")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot = TelegramToVK()
|
||||
bot.run()
|
||||
Reference in New Issue
Block a user